├── teletext2.ttf ├── teletext4.ttf ├── launchers ├── go ├── palm_tree_icon32.png ├── IMG_20200305_072500.jpg ├── artfax.desktop ├── teefax.desktop ├── wtf.desktop ├── turner.desktop ├── bbc1980.desktop ├── readback.desktop ├── ttx └── readme.txt ├── pft.config ├── LICENSE ├── vbit-remote.py ├── vbitconfig.py ├── innervision.py ├── pft.py ├── README.md ├── clut.py ├── ttxpage.py ├── vbit-iv.py ├── packet.py ├── ttxline.py └── mapper.py /teletext2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/vbit-iv/HEAD/teletext2.ttf -------------------------------------------------------------------------------- /teletext4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/vbit-iv/HEAD/teletext4.ttf -------------------------------------------------------------------------------- /launchers/go: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run the current serive 3 | 4 | cd /home/pi/vbit2/vbit-iv 5 | ./innervision.py & 6 | -------------------------------------------------------------------------------- /launchers/palm_tree_icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/vbit-iv/HEAD/launchers/palm_tree_icon32.png -------------------------------------------------------------------------------- /launchers/IMG_20200305_072500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterkvt80/vbit-iv/HEAD/launchers/IMG_20200305_072500.jpg -------------------------------------------------------------------------------- /launchers/artfax.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name= Artfax 5 | Exec=/home/pi/ttx artfax 6 | Icon=/home/pi/Pictures/palm_tree_icon32.png 7 | StartUpNotify=true 8 | Terminal=true 9 | -------------------------------------------------------------------------------- /launchers/teefax.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name= Teefax 5 | Exec=/home/pi/go 6 | Icon=/home/pi/Pictures/palm_tree_icon32.png 7 | StartUpNotify=true 8 | Terminal=false 9 | 10 | -------------------------------------------------------------------------------- /launchers/wtf.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name= WTF 5 | Exec=/home/pi/ttx wtf 6 | Icon=/home/pi/Pictures/palm_tree_icon32.png 7 | StartUpNotify=true 8 | Terminal=false 9 | 10 | -------------------------------------------------------------------------------- /pft.config: -------------------------------------------------------------------------------- 1 | 200 20 2 | 692 10 3 | 13* 20 4 | 100 15 5 | 11* 20 6 | 692 15 7 | 120 20 8 | 121 20 9 | 122 20 10 | 123 20 11 | 124 20 12 | 692 15 13 | 104 20 14 | 105 20 15 | 106 20 16 | 107 20 17 | 108 20 18 | 109 20 19 | -------------------------------------------------------------------------------- /launchers/turner.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name= Turner 5 | Exec=/home/pi/ttx turner 6 | Icon=/home/pi/Pictures/palm_tree_icon32.png 7 | StartUpNotify=true 8 | Terminal=false 9 | 10 | -------------------------------------------------------------------------------- /launchers/bbc1980.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name= BBC 1980 5 | Exec=/home/pi/ttx bbc1980 6 | Icon=/home/pi/Pictures/palm_tree_icon32.png 7 | StartUpNotify=true 8 | Terminal=false 9 | 10 | -------------------------------------------------------------------------------- /launchers/readback.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name= Readback 5 | Exec=/home/pi/ttx readback 6 | Icon=/home/pi/Pictures/palm_tree_icon32.png 7 | StartUpNotify=true 8 | Terminal=false 9 | 10 | -------------------------------------------------------------------------------- /launchers/ttx: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Displays a teletext service from a folder containing tti files. 3 | # It is most useful for running a service that isn't in vbit-config. 4 | # This is either because it doesn't have a repo associated with it 5 | # or it is a purely local service. 6 | 7 | # You must configure these file locations to match your installation. 8 | # Parameter - The name of your service in the service folder - eg. artfax 9 | # The VBIT2 stream generator executable - Wherever vbit2 was installed 10 | VBIT2="/home/pi/vbit2/vbit2" 11 | # The folder containing your teletext services - eg. /home/pi/Document/service/ 12 | SERVICE="/home/pi/Documents/service/" 13 | # The vbit=iv.py executable 14 | VBITIV="/home/pi/vbit2/vbit-iv/vbit-iv.py" 15 | $VBIT2 --dir $SERVICE$1/ | $VBITIV 1 0 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Peter Kwan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vbit-remote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2021 Peter Kwan 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | # Remote control client 24 | # Connects REQ socket to tcp://localhost:5558 25 | # Sends remote control commands to vbit-vi 26 | 27 | # pip3 install readchar 28 | # pip3 install pyzmq 29 | # 30 | import sys 31 | import zmq 32 | import time 33 | import readchar 34 | 35 | print("Connecting to vbit-iv") 36 | 37 | context = zmq.Context() 38 | 39 | # Socket to talk to server. TODO: Make the IP address and port a command line parameter 40 | # Default to local host. If you want to control vbit-iv from another computer then 41 | # set the host address to your vbit-iv machine 42 | host = "tcp://127.0.0.1:7777" 43 | #host = "tcp://192.168.1.85:7777" 44 | socket = context.socket(zmq.REQ) 45 | socket.connect(host) 46 | 47 | # main loop. Get character, send request to vbit-i 48 | # Characters that the server accepts: 49 | # Page numbers 0 to 9 50 | # h - hold 51 | # r - reveal 52 | # d - toggle double height 53 | # + - next page 54 | # - - previous page 55 | # u - red button 56 | # i - green button 57 | # o - yellow button 58 | # p - cyan button 59 | 60 | 61 | # This client can be terminated with 62 | # q or ctrl-c 63 | 64 | ch = '?' 65 | 66 | while True: 67 | ch = readchar.readchar() 68 | if ord(ch) == 3 or ch == 'q': 69 | socket.send_string(ch) 70 | exit() 71 | #print("Sending request " + str(ord(ch))) #str(key)) 72 | socket.send_string(ch) 73 | # Get the reply. 74 | #print("Sent request. Awaiting reply") 75 | message = socket.recv() 76 | #print("Received reply" + str(message[0])) 77 | -------------------------------------------------------------------------------- /vbitconfig.py: -------------------------------------------------------------------------------- 1 | # vbit-config.py 2 | # Reads vbit2 config to set up suitable launch command strings 3 | # 4 | # Copyright (c) 2020-2021 Peter Kwan 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | from pathlib import Path 25 | import json 26 | class Config: 27 | def __init__(self): 28 | # Get the sources 29 | self.HOME = str(Path.home()) 30 | self.KNOWN = self.HOME + "/vbit2/known_services" 31 | self.SERVICESDIR = self.HOME+ "/.teletext-services" 32 | self.CONFIG = self.SERVICESDIR + "/config.json" 33 | with open(self.CONFIG, 'r') as f: 34 | config_data = json.load(f) 35 | 36 | # Output: {'name': 'Bob', 'languages': ['English', 'French']} 37 | #print(data) 38 | service_name = config_data['settings']['selected'] # eg. Ceefax (London) 39 | #service = look for service_name in config_data installed 40 | service_data = list(filter(lambda x:x['name']==service_name, config_data['installed'])) 41 | print (service_data) 42 | 43 | self.service = service_name 44 | 45 | path = service_data[0]['path'] 46 | 47 | # Create the launch string. It should look something like this: 48 | # /home/peterk/vbit2/vbit2 --dir /home/peterk/.teletext-services/Teefax | ./vbit-iv.py 1 0 49 | streamer = self.HOME + '/vbit2/vbit2 ' # Run vbit ... 50 | service = ' --dir ' + path # using this service ... 51 | render = 'vbit-iv.py 1 0' # into a renderer 52 | self.launch = streamer + service + ' | ./' + render # Complete launch string with stream piped out for rendering 53 | 54 | self.service_stream = streamer + service # Launch only the streamer without rendering 55 | self.render = './' + render # Execute the render option 56 | 57 | # What is our current source? 58 | 59 | #c = Config() 60 | -------------------------------------------------------------------------------- /innervision.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Teletext In-vision viewer 4 | # 5 | # Copyright (c) 2021 Peter Kwan 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | # 25 | # 26 | # Accesses a vbit2 installation on Linux and display teletext pages. 27 | # It enables you to view teletext services with exactly the same inconvenience as a 28 | # dumb TV picking up a broadcast teletext transmission. 29 | 30 | import subprocess 31 | from vbitconfig import Config 32 | 33 | import zmq 34 | import readchar 35 | context = zmq.Context() 36 | 37 | # Remote control socket to talk to server 38 | host = "tcp://127.0.0.1:7777" 39 | # host = "tcp://192.168.1.85:7777" 40 | socket = context.socket(zmq.REQ) 41 | socket.connect(host) 42 | 43 | # Find out what the current service is called 44 | config = Config() 45 | 46 | print("The currently configured service is " + config.service) 47 | launch = config.launch 48 | 49 | try: 50 | streamIn = subprocess.Popen(config.service_stream, shell=True, stdout=subprocess.PIPE, stdin=None) 51 | streamOut = subprocess.Popen(config.render, shell=True, stdin=streamIn.stdout) 52 | 53 | # main loop. Get character, send request to vbit-iv 54 | ch = '?' 55 | while True: 56 | ch = readchar.readchar() 57 | if ord(ch) == 27 or ch == 'q': # quit with q or escape 58 | socket.send_string(ch) 59 | exit() 60 | #print("Sending request " + str(ord(ch))) #str(key)) 61 | socket.send_string(ch) 62 | # Get the reply. 63 | #print("Sent request. Awaiting reply") 64 | message = socket.recv() 65 | #print("Received reply" + str(message[0])) 66 | 67 | 68 | #except KeyboardInterrupt: # If CTRL+C is pressed, exit cleanly: 69 | # print("Innervision Keyboard interrupt") 70 | # 71 | #except Exception as inst: 72 | # print("some innervision error") 73 | #print(type(inst)) 74 | #print(inst.args) 75 | #print(inst) 76 | 77 | finally: 78 | print("iv clean up") 79 | 80 | 81 | 82 | #print(' $HOME/vbit2/vbit2 --dir /home/peterk/.teletext-services/Teefax | ./vbit-iv.py 1 0') -------------------------------------------------------------------------------- /launchers/readme.txt: -------------------------------------------------------------------------------- 1 | Launchers are clickable desktop icons that can start teletext services. 2 | 3 | A teletext service in this case is a folder with tti page files. 4 | This is installed as a part of vbit2 or you can use other sets of pages. 5 | 6 | Launchers is a kit of bits for the Raspberry Pi OS and is an accessory for 7 | vbit-iv, the teletext viewer. It is a desktop icon, some desktop 8 | shortcuts and shell scripts. 9 | 10 | Installation 11 | ============ 12 | 13 | Copy the files 14 | ============== 15 | This assumes that you have installed vbit-iv already. 16 | launchers is a subfolder of the vbit-iv project 17 | 18 | 1) Copy the icon palm_tree_icon32.png to /home/pi/Pictures 19 | 2) Copy the go and ttx scripts to your home folder /home/pi 20 | 3) Copy one of the desktop files to /home/pi/Desktop 21 | 22 | Edit the files 23 | ============== 24 | Use geany, nano or whatever editor you like. 25 | 26 | The go script is used for showing a vbit2 supported service such 27 | as Teefax. You will need to change this according to where you 28 | installed vbit-iv. 29 | 30 | #!/bin/sh 31 | # Run the current vbi2 service 32 | 33 | cd /home/pi/vbit-iv 34 | ./innervision.py & 35 | 36 | Use vbit-config to select which service you want to view. 37 | 38 | The ttx script is for running services that are not repo based and so 39 | can't be added or selected using vbit-config. The instructions for the 40 | three paths that it needs are in the ttx file. 41 | 42 | #!/bin/sh 43 | # Displays a teletext service from a folder containing tti files. 44 | # It is most useful for running a service that isn't in vbit-config. 45 | # This is either because it doesn't have a repo associated with it 46 | # or it is a purely local service. 47 | 48 | # You must configure these file locations to match your installation. 49 | # Parameter - The name of your service in the service folder - eg. artfax 50 | # The VBIT2 stream generator executable - Wherever vbit2 was installed 51 | VBIT2="/home/pi/vbit2/vbit2" 52 | # The folder containing your teletext services - eg. /home/pi/Document/service/ 53 | SERVICE="/home/pi/Documents/service/" 54 | # The vbit=iv.py executable 55 | VBITIV="/home/pi/vbit2/vbit-iv/vbit-iv.py" 56 | $VBIT2 --dir $SERVICE$1/ | $VBITIV 1 0 57 | 58 | The .desktop script comes in two flavours. The Teefax one should just 59 | run if the go script is in /home/pi. For services not managed by 60 | vbit-config you should make a new desktop launch file. 61 | This one shows a service called Turner. 62 | The Name parameter is the name on the icon. 63 | The Exec parameter is the filename of the folder. 64 | 65 | [Desktop Entry] 66 | Type=Application 67 | Encoding=UTF-8 68 | Name=Turner 69 | Exec=/home/pi/ttx turner 70 | Icon=/home/pi/Pictures/palm_tree_icon32.png 71 | StartUpNotify=true 72 | Terminal=false 73 | 74 | Optional 75 | ======== 76 | 77 | Replace the desktop image with 78 | IMG_20200305_072500.jpg 79 | and set the text colour to black. 80 | 81 | Testing 82 | ======= 83 | With so much manual intervention, things are bound to go wrong. 84 | It should be possible to automate much of this. 85 | It is also possible to test this manually to identify problems. 86 | Unfortunately I haven't written any of this. 87 | -------------------------------------------------------------------------------- /pft.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # PAGES FROM TEEFAX 4 | # Copyright (c) 2021 Peter Kwan 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | # Pages from Teefax PFT. 25 | # A remote controller for vbit.py 26 | # that goes through pages according to a list 27 | # pft.config has a very simple format 28 | # each line is a page number, a space and a time in seconds. 29 | # 104 15 30 | # 105 15 31 | # The page units can be a wild card * and all pages 0 to 9 will be shown. 32 | # 10* 15 33 | # 11* 15 34 | 35 | import zmq 36 | import time 37 | 38 | print("Connecting to vbit-iv") 39 | context = zmq.Context() 40 | host = "tcp://127.0.0.1:7777" 41 | socket = context.socket(zmq.REQ) 42 | socket.connect(host) 43 | 44 | class Reader: 45 | def __init__(self, filename): 46 | # initialise stuff in here 47 | self.line = "" 48 | self.state = 0 49 | self.filename = filename 50 | with open(filename) as f: 51 | self.content = f.readlines() 52 | self.count = len(self.content) 53 | self.pageIndex = self.count 54 | self.magazine_number = 1 55 | self.page_number_units = 0 56 | self.page_number_tens = 0 57 | self.page_wildcard = 0 58 | self.page_timing = 20 59 | def readline(self): 60 | self.pageIndex = self.pageIndex+1 61 | return self.content[self.pageIndex % self.count] 62 | def gettimer(self): # todo 63 | return 15 64 | # do the next step in the sequence 65 | def step(self): 66 | if self.state == 0: # mag 67 | if self.page_number_units != '*': 68 | # read next line, if not still wildcarding 69 | x = self.readline() 70 | # extract the page from the value. eg. "400 15" 71 | self.magazine_number = x[0] 72 | self.page_number_tens = x[1] 73 | self.page_number_units = x[2] 74 | t = x[4:] 75 | self.page_timing = int(t) 76 | print ("NEXT PAGE. Mag = " + self.magazine_number) 77 | self.state = self.state+1 78 | return self.magazine_number 79 | if self.state == 1: # page tens 80 | self.state = self.state+1 81 | print ("NEXT PAGE. Tens = " + self.page_number_tens) 82 | return self.page_number_tens 83 | if self.state == 2: # page units 84 | unit = self.page_number_units 85 | print("wildcard-1. unit = " + unit) 86 | if unit == '*': 87 | unit = str(self.page_wildcard) 88 | self.page_wildcard = self.page_wildcard + 1 89 | if self.page_wildcard >= 10: # terminate the wildcard 90 | self.page_wildcard = 0 91 | self.page_number_units = 'x' 92 | self.state = self.state + 1 93 | print ("NEXT PAGE. Units = " + unit) 94 | return unit 95 | if self.state == 3: # Delay for page time then reset ready for next page 96 | time.sleep(self.page_timing) 97 | self.state = 0 98 | return " " 99 | 100 | reader = Reader("pft.config") 101 | while True: 102 | socket.send_string(reader.step()) 103 | message = socket.recv() 104 | print("Received reply" + str(message[0])) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vbit-iv 2 | ## In-vision teletext display 3 | 4 | Intended use. To get the full teletext experience without a television. For generating teletext displays on a Linux PC. To display teletext on any output device and not just in 625 line PAL. 5 | 6 | It takes the teletext stream generated by vbit2 and decodes this and displays it full frame. It displays teletext pages on a Linux PC or Raspberry Pi. In this way you can display live teletext on any display that you have connected. This opens the possibiliy of using a projector for presentations and exhibitions. 7 | 8 | By using vbit2 you can get a live teletext feed from a number of channels including Teefax and Chunkytext. 9 | 10 | A network server listens for remote control commands so you can select pages by sending the appropriate commands. vbit-remote.py is a simple client that takes keyboard commands and sends them to vbit-iv. You can write your own client to work a remote control from a mobile phone for example. 11 | 12 | # Installation 13 | 14 | Head on over to the Wiki page https://github.com/peterkvt80/vbit-iv/wiki for current instructions. 15 | 16 | Below are older instructions. 17 | 18 | Requirements: Linux operating system. Tested with Ubuntu and Raspberry Pi OS. Python 3. VBIT2 teletext streamer. 19 | It may work on Windows if you use the Windows version of VBIT2 but you are on your own! 20 | 21 | * Install VBIT2 as detailed in the Github project. Use vbit-config to add services and select one to display. 22 | * Download the vbit-iv ZIP and unpack it somewhere convenient OR better still, clone it if you want to keep the code up to date. 23 | * Click on the font files and install them onto your system. These are teletext2.ttf and teletext4.ttf. For Raspberry Pi OS just copy them to the fonts folder. 24 | * You also need to install screeninfo from PyPI using a python package manager or type 25 | 26 | pip3 install screeninfo 27 | 28 | # Running 29 | Move to your vbit-iv/ directory. There are a number of ways that you could run the code. 30 | ## Python 31 | The easiest method is to run the in-vision script as this starts everything and starts the vbit2 configured service. 32 | 33 | ./innervision.py 34 | 35 | You can type commands directly onto the teletext page, or into the shell that you launched the program from. In the case of the shell, it sends the comands through the remote control port so you can't attach another remote control client to it. 36 | 37 | To change the service, use the vbit configuration utility. You can manages services here including adding, selecting and updating. Don't use the Start VBIT2 option as the in vision viewer doesn't need it. 38 | 39 | vbit-config 40 | 41 | ## Command line 42 | $HOME/vbit2/vbit2 --dir $HOME/.teletext-services/Teefax | ./vbit-iv.py 1 0 43 | 44 | This command starts vbit2 with the Teefax service. Unlike the python startup script this doesn't use the vbit2 config system. You need to give it the name of one of the service that you installed using vbit-config (Ceefax, Chunkytext, SPARK, Teefax etc.). The last two numbers are the initial magazine (1..8) and the page number (0..99) 45 | 46 | 47 | ## Commands 48 | The keyboard is used as a remote control. You may need to click on the screen first to get focus. 49 | * 0 to 9. Select page number. 50 | * h - Hold toggle 51 | * r - Reveal-oh toggle 52 | * u,i,o,p - Fastkeys Red, Green, Yellow, Cyan. 53 | * q - Close down the viewer 54 | * plus/minus - Next/Previous page (not implemented) 55 | 56 | ## Remote control 57 | The viewer has a network remote control on port 7777. vbit-remote.py is a suitable client. It uses the same commands as the keyboard. You can edit the code to change your host if you want to run the remote on another computer. To use it just run the viewer and type the commands into the shell you ran it from. 58 | 59 | ./vbit-remote.py 60 | 61 | Another remote control is "Pages from Teefax". This sequences a series of pages. First create a file called pft.config. Each line is a three digit page number, a space and then a timing. The example below shows all of the BBC news pages in Teefax. Note that the units of the page number can be replaced by a wildcard to display all the pages (in this case) 110 to 119 62 | 63 | 104 20 64 | 105 20 65 | 106 20 66 | 107 20 67 | 108 20 68 | 109 20 69 | 11* 20 70 | 120 20 71 | 121 20 72 | 122 20 73 | 123 20 74 | 134 20 75 | 76 | Run the remote control with 77 | ./pft.py 78 | 79 | You can write your own remote control, for controlling vbit-iv. It only takes a little bit of Python coding. Some ideas for you: A voice activated page selector. A carousel of the last ten updated pages. A default page reset. The system always returns to the same page every five minutes, so people can browse but it resets. Any more ideas? 80 | 81 | # FLIRC 82 | FLIRC stands for Linux Infra Red Control. I'd rather not say what the F stands for. FLIRC is a tiny USB receiver that picks up infra red commands and converts them into keyboard key presses. So for example, you can program the Reveal button on your remote control so that it generates an "r" key press. 83 | 84 | It is a really great if a bit pricey device once you have it set up. However, the setup software is a bad experience all round. Once you have the software installed and running you can program your FLIRC. Choose the full keyboard option and programme the keys listed in the *Commands* section with your remote control. 85 | 86 | Fortunately you only need to do this once for when the keys are programmed, you can move the FLIRC to any PC, laptop, Raspberry Pi or whatever you've got that can be controlled by a USB keyboard. 87 | -------------------------------------------------------------------------------- /clut.py: -------------------------------------------------------------------------------- 1 | # clut.py. 2 | # 3 | # clut.py Teletext colour lookup table 4 | # Maintains colour lookups 5 | # 6 | # Copyright (c) 2020 Peter Kwan 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | # 26 | 27 | # This holds the colour lookup tables read in by packet 28 etc. 28 | # I think we have four CLUTs 0 to 3. Here is what the standard says: 29 | ## 8 background full intensity colours: 30 | ## Magenta, Cyan, White. Black, Red, Green, Yellow, Blue, 31 | ## 7 foreground full intensity colours: 32 | ## Cyan, White. Red, Green, Yellow, Blue, Magenta, 33 | ## Invoked as spacing attributes via codes in packets X/0 to X/25. 34 | ## Black foreground: Invoked as a spacing attribute via codes in packets X/0 35 | ## to X/25. 36 | ## 32 colours per page. The Colour Map contains four CLUTs 37 | ## (numbered 0 - 3), each of 8 entries. Each entry has a four bit resolution for 38 | ## the RGB components, subclause 12.4. 39 | ## Presentation 40 | ## Level 41 | ## 1 1.5 2.5 3.5 42 | ## { { ~ ~ 43 | ## ~ ~ ~ ~ 44 | ## { { ~ ~ 45 | ## { { ~ ~ 46 | ## Colour Definition 47 | ## CLUT 0 defaults to the full intensity colours used as spacing colour 48 | ## attributes at Levels 1 and 1.5. 49 | ## CLUT 1, entry 0 is defined to be transparent. CLUT 1, entries 1 to 7 default 50 | ## to half intensity versions of CLUT 0, entries 1 to 7. 51 | ## CLUTs 2 and 3 have the default values specified in subclause 12.4. CLUTs 52 | ## 2 and 3 can be defined for a particular page by packet X/28/0 Format 1, or 53 | ## for all pages in magazine M by packet M/29/0. 54 | ## Colour Selection 55 | ## CLUT 0, entries 1 to 7 are selectable directly by the Level 1 data as 56 | ## spacing attributes. CLUTs 0 to 3 are selectable via packets 26 or objects 57 | ## as non-spacing attributes. 58 | ## The foreground and background colour codes on the Level 1 page may be 59 | ## used to select colours from other parts of the Colour Map. Different CLUTs 60 | ## may be selected for both foreground and background colours. 61 | ## This mapping information is transmitted in packet X/28/0 Format 1 for the 62 | ## associated page and in packet M/29/0 for all pages in magazine M. 63 | ## With the exception of entry 0 in CLUT 1 (transparent), CLUTs 0 and 1 can 64 | ## be redefined for a particular page by packet X/28/4, or 65 | ## 66 | 67 | class Clut: 68 | def __init__(self): 69 | print ("Clut loaded") 70 | self.clut0 = [0] * 8 # Default full intensity colours 71 | self.clut1 = [0] * 8 # default half intensity colours 72 | self.clut2 = [0] * 8 73 | self.clut3 = [0] * 8 74 | # set defaults 75 | self.reset() 76 | 77 | # Used by X26/0 to swap entire cluts 78 | # @param colour - Colour index 0..7 79 | # @param remap - Remap 0..7 80 | # @param foreground - True for foreground coilour, or False for background 81 | # @return - Colour string for tkinter. eg. 'black' or '#000' 82 | def RemapColourTable(self, colourIndex, remap, foreground): 83 | if type(colourIndex) != int: 84 | print('[RemapColourTable] colourIndex is not an integer' + colourIndex + ". foreground = " +str(foreground)) 85 | clutIndex = 0 86 | if foreground: 87 | if remap>4: 88 | clutIndex = 2 89 | elif remap<3: 90 | clutIndex = 0 91 | else: 92 | clutIndex = 1 93 | else: # background 94 | if remap < 3: 95 | clutIndex = remap 96 | elif remap == 3 or remap == 5: 97 | clutIndex = 1 98 | elif remap == 4 or remap == 6: 99 | clutIndex = 2 100 | else: 101 | clutIndex = 3 102 | return self.get_value(clutIndex, colourIndex) 103 | 104 | 105 | def reset(self): # To values from table 12.4 106 | # CLUT 0 full intensity 107 | self.clut0[0] = '#000' # black 108 | self.clut0[1] = '#f00' # red 109 | self.clut0[2] = '#0f0' # green 110 | self.clut0[3] = '#ff0' # yellow 111 | self.clut0[4] = '#00f' # blue 112 | self.clut0[5] = '#f0f' # magenta 113 | self.clut0[6] = '#0ff' # cyan 114 | self.clut0[7] = '#fff' # white 115 | 116 | # CLUT 1 half intensity 117 | self.clut1[0] = '#000' # transparent 118 | self.clut1[1] = '#700' # half red 119 | self.clut1[2] = '#070' # half green 120 | self.clut1[3] = '#770' # half yellow 121 | self.clut1[4] = '#007' # half blue 122 | self.clut1[5] = '#707' # half magenta 123 | self.clut1[6] = '#077' # half cyan 124 | self.clut1[7] = '#777' # half white 125 | 126 | # CLUT 2 lovely colours 127 | self.clut2[0] = '#f05' # crimsonish 128 | self.clut2[1] = '#f70' # orangish 129 | self.clut2[2] = '#0f7' # blueish green 130 | self.clut2[3] = '#ffb' # pale yellow 131 | self.clut2[4] = '#0ca' # cyanish 132 | self.clut2[5] = '#500' # dark red 133 | self.clut2[6] = '#652' # hint of a tint of runny poo 134 | self.clut2[7] = '#c77' # gammon 135 | 136 | # CLUT 3 more lovely colours 137 | self.clut3[0] = '#333' # pastel black 138 | self.clut3[1] = '#f77' # pastel red 139 | self.clut3[2] = '#7f7' # pastel green 140 | self.clut3[3] = '#ff7' # pastel yellow 141 | self.clut3[4] = '#77f' # pastel blue 142 | self.clut3[5] = '#f7f' # pastel magenta 143 | self.clut3[6] = '#7ff' # pastel cyan 144 | self.clut3[7] = '#ddd' # pastel white 145 | 146 | 147 | # set a value in a particular clut 148 | # Get the colour from a particular clut 149 | # Probably want to record which cluts are selected 150 | # Lots of stuff 151 | 152 | # @param colour - 12 bit web colour string eg. '#1ab' 153 | # @param clut_index CLUT index 0 to 3 154 | # @param clr_index - 0..7 colour index 155 | def set_value(self, colour, clut_index, clr_index): 156 | clr_index = clr_index % 8 # need to trap this a bit better. This is masking a problem 157 | clut_index = clut_index % 4 158 | if clut_index==0: 159 | self.clut0[clr_index] = colour; 160 | if clut_index==1: 161 | self.clut1[clr_index] = colour; 162 | if clut_index==2: 163 | self.clut2[clr_index] = colour; 164 | if clut_index==3: 165 | self.clut3[clr_index] = colour; 166 | print("clut value: clut" + str(clut_index) + " set[" + str(clr_index) + '] = ' + colour) 167 | 168 | # @return colour - 12 bit web colour string eg. '#1ab' 169 | # @param clut_index CLUT index 0 to 3 170 | # @param clr_index - 0..7 colour index 171 | def get_value(self, clut_index, clr_index): 172 | clut_index = clut_index % 4 173 | clr_index = clr_index % 8 174 | if clut_index == 0: 175 | return self.clut0[clr_index] 176 | if clut_index == 1: 177 | return self.clut1[clr_index] 178 | if clut_index == 2: 179 | return self.clut2[clr_index] 180 | if clut_index == 3: 181 | return self.clut3[clr_index] 182 | return 0 # just in case! 183 | 184 | # debug dump the clut contents 185 | def dump(self): 186 | print("[Dump] CLUT values") 187 | for i in range(8): 188 | print(self.clut0[i] + ', ', end='') 189 | print() 190 | for i in range(8): 191 | print(self.clut1[i] + ', ', end='') 192 | print() 193 | for i in range(8): 194 | print(self.clut2[i] + ', ', end='') 195 | print() 196 | for i in range(8): 197 | print(self.clut3[i] + ', ', end='') 198 | print() 199 | 200 | clut = Clut() 201 | -------------------------------------------------------------------------------- /ttxpage.py: -------------------------------------------------------------------------------- 1 | # ttxpage.py. 2 | # 3 | # VBIT Stream renderer. Teletext page level. 4 | # You can modify this to run full frame 5 | # or a smaller window 6 | # 7 | # Copyright (c) 2020-2021 Peter Kwan 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | 27 | from tkinter import Tk 28 | from ttxline import TTXline 29 | 30 | 31 | import screeninfo 32 | 33 | class TTXpage: 34 | print("TTXPage created") 35 | 36 | def get_monitor_from_coord(self, x, y): 37 | monitors = screeninfo.get_monitors() 38 | 39 | for m in reversed(monitors): 40 | if m.x <= x <= m.width + m.x and m.y <= y <= m.height + m.y: 41 | return m 42 | return monitors[0] 43 | 44 | 45 | 46 | 47 | def __init__(self): 48 | self.root = Tk() 49 | 50 | # Get the screen which contains top 51 | current_screen = self.get_monitor_from_coord(self.root.winfo_x(), self.root.winfo_y()) 52 | 53 | # Get the monitor's size 54 | self.width_value = current_screen.width 55 | self.height_value = current_screen.height 56 | 57 | self.root.configure(background='black', borderwidth=0, highlightthickness=0) 58 | geometry = "%dx%d+0+0" % (self.width_value, self.height_value) 59 | print('[ttxpage::_init__] geometry = ' + geometry) 60 | 61 | print(' GEOMETRY = ' + geometry) 62 | self.root.geometry(geometry) 63 | 64 | # Make it full screen (Comment it out if you want to run in a window) 65 | self.root.wm_attributes('-fullscreen','true') 66 | 67 | self.root.wait_visibility(self.root) 68 | 69 | # lines 70 | self.lines = TTXline(self.root, self.height_value) 71 | self.lines.text.pack() 72 | 73 | self.root.update_idletasks() 74 | self.root.update() 75 | 76 | self.mag=[None] * 4 77 | self.page=[None] * 4 78 | 79 | self.root.bind('', self.onKeyPress) 80 | 81 | self.buffer = [] 82 | 83 | # Level 1.5 character replacement 84 | self.rowAddr = 0 85 | self.colAddr = 0 86 | print("ttiPage constructor exits") 87 | 88 | 89 | # Return the page number for the link selected by index 90 | def getPage(self, index): 91 | return self.page[index] 92 | 93 | # Return the magazine number for the link selected by index 94 | def getMag(self, index): 95 | return self.mag[index] 96 | 97 | def deham(self, value): 98 | # Deham with NO checking! @todo Parity and error correction 99 | b0 = (value & 0x02) >> 1 100 | b1 = (value & 0x08) >> 2 101 | b2 = (value & 0x20) >> 3 102 | b3 = (value & 0x80) >> 4 103 | return b0+b1+b2+b3 104 | 105 | # return True if the packet contained double height 106 | def printRow(self, packet, row): 107 | if row < 0 or row > 24 : 108 | return False 109 | return self.lines.printRow(packet, row) 110 | 111 | def printHeader(self, packet, page, seeking, suppress = False): 112 | self.lines.printHeader(packet, page, seeking, suppress) 113 | 114 | # Actually draw the stuff 115 | def mainLoop(self): 116 | self.root.update_idletasks() 117 | self.root.update() 118 | 119 | def toggleReveal(self): 120 | self.lines.toggleReveal() 121 | 122 | # decode packet 27 fastext links. @TODO MOVE TO PACKET 123 | def decodeLinks(self, packet): 124 | offset = 2 125 | dc = self.deham(packet[6 + offset]) # designation code 126 | # print ("packet 27 dc = " + str(dc)) 127 | # @todo extract the row 24 display 128 | for i in range(4): 129 | mag = self.deham(packet[0]) &0x07 # Magazine of this packet 130 | addr = (i-1) * 6 + 7 + offset 131 | b1 = self.deham(packet[addr]) 132 | b2 = self.deham(packet[addr+1]) 133 | M1 = self.deham(packet[addr+3]) & 0x08 # relative magazie of target link M1, M2, M3 134 | M2 = self.deham(packet[addr+5]) & 0x04 135 | M3 = self.deham(packet[addr+5]) & 0x08 136 | tMag = mag 137 | if M1: 138 | tMag = tMag ^ 0x01 139 | if M2: 140 | tMag = tMag ^ 0x02 141 | if M3: 142 | tMag = tMag ^ 0x04 143 | if tMag == 0: 144 | tMag = 8 145 | page = b2 * 0x10 + b1 146 | #print("mag = " + hex(mag) + ", Target tMag = " + hex(tMag) + ", " + hex(b1) + ", " + hex(b2)) 147 | #print("link " + str(i) + " = " + str(tMag) + " " + hex(page) ) 148 | self.mag[i] = tMag 149 | self.page[i] = page 150 | 151 | # @todo Calculate the relative magazine 152 | 153 | 154 | def reverse(self, x): # reverse the bit order in a byte 155 | return 156 | x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4) 157 | x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2) 158 | x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1) 159 | return x 160 | 161 | def onKeyPress(self, event): 162 | self.buffer.append(event.char) 163 | if event.char!='': 164 | print("[ttxPage::onKeyPress] You pressed " + str(ord(event.char))) 165 | 166 | def getKey(self): 167 | if self.buffer: 168 | key = self.buffer.pop(0) 169 | if key != '': 170 | print("[ttxPage::getKey] key == " + str(ord(key))) 171 | if key == 105: # Mappings F1 172 | key = 'P' 173 | return key 174 | else: 175 | key = ' ' 176 | return key 177 | 178 | def dumpPacket(self, pkt): 179 | for i in range(8): 180 | print(str(i) + ":" + hex(pkt[i]) + ' ', end='') 181 | print() 182 | 183 | ### IN PROGRESS: Move packet handling to packet.py 184 | def decodeRow26(self, pkt): 185 | return # This is moved to packet 186 | # There is a lot of stuff in X26. Initially just look at diacriticals 187 | #self.dumpPacket(pkt) 188 | dc = self.deham(pkt[2]) 189 | tp = self.decodeTriplets(pkt) 190 | print("Packet 26 DC = " + str(dc)) 191 | for i in range (0, 12): 192 | x = tp[i] # self.getTriplet(i, pkt) 193 | data = (x >> 11) & 0x7f 194 | mode = (x >> 6) & 0x1f 195 | address = x & 0x3f 196 | if mode != 0x1f: # filter out termination marker 197 | print("Packet 26 triplet = " + str(i) + " data = " + hex(data) + " mode = " + hex(mode) + " address = " + str(address), end='') 198 | if address>=40 and address<=63: # It is a row address group 199 | modeStr = { 200 | 0x01: "Full row colour", 201 | 0x02: "Reserved", 202 | 0x03: "Reserved", 203 | 0x04: "Set Active Position", 204 | 0x05: "Reserved", 205 | 0x06: "Reserved", 206 | 0x07: "Address display row0", 207 | 0x08: "PDC Data - Country", 208 | 0x09: "PDC Data - Month and day", 209 | 0x0a: "PDC Data - Cursor row, start time", 210 | 0x0b: "PDC Data - Cursor row, end time", 211 | 0x0c: "PDC", 212 | 0x0d: "PDC", 213 | 0x0e: "Reserved", 214 | 0x0f: "Reserved", 215 | 0x12: "Adaptive Object Invocation", 216 | 0x19: "Reserved", 217 | 0x1a: "Reserved", 218 | 0x1b: "Reserved", 219 | 0x1c: "Reserved", 220 | 0x1d: "Reserved", 221 | 0x1e: "Reserved", 222 | 0x1f: "Termination marker", 223 | } 224 | if address>=0 and address<=39: # It is a column address group 225 | modeStr = { 226 | 0x00: "Foreground colour", 227 | 0x01: "Block mosaic character G1", 228 | 0x02: "Smoothed mosaic G3", 229 | 0x03: "Background colour", 230 | 0x04: "Reserved", 231 | 0x05: "Reserved", 232 | 0x06: "PDC - Cursor column", 233 | 0x07: "Additional flash functions", 234 | 0x08: "Modified G0/G2 character set", 235 | 0x09: "Character from G0 set (2.5, 3.5)", 236 | 0x0a: "Reserved", 237 | 0x0b: "Line drawing or smoothed mosaic G3 set (2.5, 3.5)", 238 | 0x0c: "Display attributes", 239 | 0x0d: "DRCS character invocation", 240 | 0x0e: "Font style", 241 | 0x0f: "Character from the G2 set", 242 | 0x10: "G0 character without diacritical mark", 243 | 0x11: "G0 character with diacritical mark", 244 | 0x12: "G0 character with diacritical mark", 245 | 0x13: "G0 character with diacritical mark", 246 | 0x14: "G0 character with diacritical mark", 247 | 0x15: "G0 character with diacritical mark", 248 | 0x16: "G0 character with diacritical mark", 249 | 0x17: "G0 character with diacritical mark", 250 | 0x18: "G0 character with diacritical mark", 251 | 0x19: "G0 character with diacritical mark", 252 | 0x1a: "G0 character with diacritical mark", 253 | 0x1b: "G0 character with diacritical mark", 254 | 0x1c: "G0 character with diacritical mark", 255 | 0x1d: "G0 character with diacritical mark", 256 | 0x1e: "G0 character with diacritical mark", 257 | 0x1f: "G0 character with diacritical mark", 258 | } 259 | if mode == 0x1f: # A termination marker, nothing more to come 260 | return 261 | print (" mode = " + modeStr.get(mode, hex(mode))) 262 | if address>=40 and address<=63: # It is a row address group 263 | # @todo Modes between 0 and 0x1f 264 | #When data field D6 and D5 are both set to '0', bits D4 - D0 define the background 265 | #colour. Bits D4 and D3 select a CLUT in the Colour Map of table 30, and bits D2 - 266 | #D0 select an entry from that CLUT. All other data field values are reserved. 267 | #The effect of this attribute persists to the end of a display row unless overridden 268 | #by either a spacing or a non-spacing attribute defining the background colour. 269 | if mode == 0x04: # Set Active Position 270 | self.rowAddr = address - 40 271 | if data<40: 272 | self.colAddr = data 273 | # print("rowAddr = " + str(self.rowAddr)) 274 | if address>=0 and address<=39: # It is a column address group 275 | # @todo Modes between 0 and 0x1f 276 | if mode == 0x03: #Background colour 277 | if (data & 0x60) == 0: 278 | clut = (data >> 3) & 0x03 279 | ix = data & 0x07 280 | print ("set background colour at (" + str(self.rowAddr) + ", " + str(self.colAddr) + ") to " + str(clut) + "[" + str(ix) +']') 281 | if mode & 0x10 and mode != 0x1f: # G0 character with diacritical mark 282 | self.colAddr = address 283 | dia = int(mode & 0x0f) 284 | mapChar = tuple((self.rowAddr, self.colAddr, dia)) 285 | self.lines.addMapping(mapChar) 286 | # print("mapChar = " + str(mapChar[0]) + " " + str(mapChar[1]) + " " + str(mapChar[2]) + " ") 287 | 288 | # Set a flag to clear down when starting the next page 289 | def clear(self): 290 | self.lines.clear("new page") 291 | -------------------------------------------------------------------------------- /vbit-iv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # T42 Teletext Stream to In-vision decoder 4 | # 5 | # Copyright (c) 2020-2021 Peter Kwan 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | print('VBIT-iv System started') 25 | 26 | import sys 27 | import time 28 | from ttxpage import TTXpage 29 | import zmq 30 | from packet import Packet, metaData 31 | from clut import clut, Clut 32 | 33 | 34 | # Globals 35 | packetSize=42 # The input stream packet size. Does not include CRI and FC 36 | 37 | # buffer stuff 38 | head=0 39 | tail=0 40 | 41 | # decoder state 42 | currentMag=1#4 43 | currentPage=0x00#0x70 44 | capturing = False # True while we are accepting rows for the selected page 45 | wasCapturing = False # True if last row was accepted. Used to detect when a page load is complete. 46 | elideRow = 0 47 | seeking = True # True while seeking a new page to display 48 | lastPacket = b"AB0123456789012345678901234567890123456789" 49 | holdMode = False 50 | subCode = 0 # The current page subcode 51 | lastSubcode = 0 # The previous carousel page subcode 52 | rowCounter = 0 # Rows count for this page so far, except for rows after Double Height 53 | 54 | suppressHeader = False 55 | 56 | # remote 57 | pageNum = "100" 58 | 59 | ttx = TTXpage() 60 | 61 | print(sys.argv) 62 | 63 | # Accept mag and page eg. .vbitvid 1 29 64 | if int(sys.argv[1])>0: 65 | currentMag = int(sys.argv[1]) % 8 66 | print ("mag = "+str(currentMag)) 67 | if int(sys.argv[2])>0: 68 | currentPage = int(sys.argv[2], 16) 69 | print ("page = "+str(currentPage)) 70 | 71 | def dump(pkt, row): 72 | print("dump row = "+str(row)) 73 | print(pkt.hex()) 74 | 75 | def deham(value): 76 | # Deham with NO checking! @todo Parity and error correction 77 | b0 = (value & 0x02) >> 1 78 | b1 = (value & 0x08) >> 2 79 | b2 = (value & 0x20) >> 3 80 | b3 = (value & 0x80) >> 4 81 | return b0+b1+b2+b3 82 | 83 | def mrag(v1, v2): 84 | rowlsb = deham(v1) 85 | mag = rowlsb % 8 86 | if mag==0: 87 | mag = 8 88 | 89 | row = deham(v2) << 1 90 | if (rowlsb & 0x08)>0: 91 | row = row + 1 92 | return mag, row 93 | 94 | def decodePage(pkt): 95 | tens = deham(pkt[3]) 96 | units = deham(pkt[2]) 97 | return tens * 0x10 + units 98 | 99 | def decodeSubcode(pkt): 100 | s1 = deham(pkt[4]) 101 | s2 = deham(pkt[5]) & 0x07 102 | s3 = deham(pkt[6]) 103 | s4 = deham(pkt[7]) & 0x03 104 | return (s4 << 11) + (s3 << 7) + (s2 << 4) + s1 105 | 106 | def getC7(pkt): # C7 - Suppress header 107 | s1 = deham(pkt[8]) 108 | return s1 & 0x01 109 | 110 | def remote(ch): 111 | global pageNum 112 | global currentMag 113 | global currentPage 114 | global lastPacket 115 | global seeking 116 | global holdMode 117 | if ch == '': 118 | return 119 | if ch == 'h': # hold 120 | holdMode = not holdMode 121 | return 122 | if ch == 'r': # reveal-oh 123 | ttx.toggleReveal() 124 | return 125 | if ch == 'q' or ord(ch) == 27: # quit 126 | exit() 127 | if ch == 'P' or ch == 'u': # f1 red link 128 | currentMag = ttx.getMag(0) 129 | currentPage = ttx.getPage(0) 130 | print(str(currentMag) + " " + hex(currentPage)) 131 | seeking = True 132 | ttx.clear() # Doubt we want to do this! 133 | return 134 | if ch == 'Q' or ch == 'i': # f2: green link 135 | currentMag = ttx.getMag(1) 136 | currentPage = ttx.getPage(1) 137 | print(str(currentMag) + " " + hex(currentPage)) 138 | seeking = True 139 | return 140 | if ch == 'R' or ch == 'o': # f3 yellow link 141 | currentMag = ttx.getMag(2) 142 | currentPage = ttx.getPage(2) 143 | print(str(currentMag) + " " + hex(currentPage)) 144 | seeking = True 145 | return 146 | if ch == 'S' or ch == 'p': # f4 cyan link 147 | currentMag = ttx.getMag(3) 148 | currentPage = ttx.getPage(3) 149 | print(str(currentMag) + " " + hex(currentPage)) 150 | seeking = True 151 | return 152 | if ch>='0' and ch<='9': 153 | pageNum = pageNum + ch 154 | pageNum = pageNum[1:4] 155 | print ("Pagenum= " + pageNum) 156 | # validate 157 | if pageNum[0]>'0' and pageNum[0]<'9': # valid mag 158 | currentMag = int(pageNum[0]) 159 | currentPage = int(pageNum[1:3],16) 160 | print ("mag, page = " + str(currentMag)+', '+hex(currentPage)) 161 | # send the last header again, just so we can update the target page number 162 | seeking = True # @todo If we select the page we are already on 163 | page_number = 'P' + pageNum 164 | print("page number = " + page_number) 165 | ttx.printHeader(lastPacket, page_number+' ', seeking, False) 166 | if ch=='d': 167 | metaData.dump() 168 | else: 169 | print("[vbit-iv] Unhandled remote code: " + ch) 170 | # @todo Reveal, Fastext, Hold, Double height, Page up, Page Down, Mix 171 | #if seeking: 172 | # ttx.clear() 173 | 174 | # \param pkt - raw T42 packet to process 175 | def process(pkt): 176 | global capturing 177 | global wasCapturing 178 | global currentMag 179 | global currentPage 180 | global elideRow 181 | global rowCounter # Counts the rows, except the rows following a double height row 182 | global lastPacket 183 | global seeking 184 | global holdMode 185 | global subCode 186 | global lastSubcode 187 | global clut 188 | global suppressHeader 189 | 190 | if len(pkt) < 42: # Quit if we don't have a full packet 191 | print("invalid teletext packet") 192 | exit() 193 | result = mrag(pkt[0], pkt[1]) 194 | mag = result[0] 195 | row = result[1] 196 | # If this is a header, decode the page 197 | 198 | # only display things that are on our magazine 199 | if currentMag == mag: # assume parallel mode 200 | if row == 0: # Is this a header? 201 | elideRow = 0 # new header, cancel any elide that might have happened 202 | if holdMode: 203 | ttx.printHeader(lastPacket, "HOLD ", False, False) 204 | return 205 | # print("\033[0;0fP", end='') 206 | # is this the page that we want? 207 | page = decodePage(pkt) 208 | subcode = decodeSubcode(pkt) # Used to clear down page if changed. Also clears X26 char map 209 | # This is where we need to grab transmission flags @todo 210 | capturing = currentPage == page 211 | if capturing: 212 | rowCounter = 0 213 | seeking = False # Capture starts if this is the right page 214 | lastPacket = pkt 215 | suppressHeader = getC7(pkt)>0 216 | if subcode != lastSubcode: 217 | lastSubcode = subcode 218 | ttx.clear() 219 | wasCapturing = True 220 | else: 221 | # If we have fewer rows because of double height, we must blank the extra lines 222 | if wasCapturing: 223 | wasCapturing = False 224 | print("[vbit-iv::process] Page load completed. rowCounter = " + str(rowCounter)) 225 | # Got rowCounter lines when we expected 226 | #if rowCounter<24: # THIS DOESN'T WORK :-( 227 | #for i in range(rowCounter+2, 24+2): 228 | #print("[vbit-iv process] erase line " + str(i)) 229 | #ttx.printRow(b"QQxxxxxxxxxxyyyyyyyyyyzzzzzzzzzzkkkkkkkkkk", 24) 230 | if seeking: 231 | suppressHeader = False 232 | 233 | #ttx.lines.clearX26() 234 | clut.reset() # @todo Do we need to save colours in some cases? 235 | # print("sub-code = " + hex(subcode)) 236 | 237 | 238 | #if not seeking: # new header starts rendering the page 239 | # clearPage() # @todo Decode header flagsallow-hotplug can0 240 | 241 | # @todo Don't clear if the page is already loaded 242 | #printRow(pkt, 0, 0, "P{:1d}{:02X} ".format(currentMag,currentPage)) 243 | elideRow = 0 244 | # Show the whole header if we are capturing. Otherwise just show the clock 245 | ttx.printHeader(pkt, "P{:1d}{:02X} ".format(currentMag,currentPage), seeking, suppressHeader) 246 | #if capturing: 247 | #printRow(packet, 0, 0, "{:1d}{:02X}".format(mag,page)) 248 | # print("\033[2J", end='') # clear screen 249 | #printRow(packet) 250 | else: # not a header 251 | #print("TRACE GA") 252 | # If we hit a row that follows a double height, skip the packet 253 | # Deeply suspect that this is removing too many rows 254 | if elideRow>0 and elideRow == row: 255 | ttx.mainLoop() 256 | print("[vbit-iv]eliding row= " + str(elideRow) + " rowCounter = " + str(rowCounter)) 257 | elideRow=0 258 | return 259 | 260 | 261 | # @todo Need to copy all pages until a new header arrives 262 | if capturing: 263 | if row < 25: 264 | #dump(pkt, row) 265 | if ttx.printRow(pkt, row): # printable row. Is it double height? 266 | elideRow = row + 1 267 | rowCounter = rowCounter + 1 # increment the printed row count 268 | # print("[vbit-iv] row = " + str(row) + " row length ="+ str(len(pkt))) 269 | if row == 26: 270 | # ttx.decodeRow26(pkt) # @todo TO BE REPLACED 271 | print("process packet26 called") 272 | metaData.decode(pkt, 26) 273 | if row == 27: # fastext 274 | ttx.decodeLinks(pkt) # @todo TO BE REPLACED 275 | if row == 28: # region 276 | # ttx.decodeRow28(pkt) # @todo TO BE REPLACED 277 | metaData.decode(pkt, 28) 278 | if row == 29: # 279 | print("Unsupported packet type = " + str(row)) 280 | if row == 30: # 281 | print("Unsupported packet type = " + str(row)) 282 | if row == 31: # 283 | print("Unsupported packet type = " + str(row)) 284 | # Page 32 285 | #ETS 300 706: May 1997 286 | ttx.mainLoop() 287 | 288 | # Local control 289 | 290 | 291 | # Remote control talks on port 6558 292 | bind = "tcp://*:7777" 293 | print("vbit-vi binding to " + bind) 294 | context = zmq.Context() 295 | socket = context.socket(zmq.REP) 296 | socket.bind(bind) 297 | try: 298 | # This thread reads the input stream into a field buffer 299 | while True: 300 | # load a field of 16 vbi lines 301 | #print("x") 302 | for line in range(16): 303 | # packet=file.read(packetSize) # file based version 304 | packet=sys.stdin.buffer.read(packetSize) # read binary from stdin 305 | if len(packet)<42: 306 | print ("No source data. (Check vbit2 and the configured teletext service)") 307 | else: 308 | process(packet) 309 | #print("y") 310 | # see if the keyboard has received a remote control code 311 | key = ttx.getKey() 312 | if key != ' ': 313 | if key == 'q' or key == 27: 314 | exit() 315 | remote(key) 316 | time.sleep(0.020) # 20ms between fields 317 | 318 | 319 | try: 320 | #message = socket.recv() 321 | message = socket.recv(flags=zmq.NOBLOCK) 322 | message = message.decode("utf-8") 323 | remote(message) 324 | #print("Message received " + message) 325 | socket.send(b"sup?") 326 | except zmq.Again as e: 327 | time.sleep(0.001) # do nothing 328 | 329 | except KeyboardInterrupt: # If CTRL+C is pressed, exit cleanly: 330 | print("Keyboard interrupt") 331 | 332 | # except Exception as inst: 333 | # print("vbit-iv error") 334 | # print(type(inst)) 335 | # print(inst.args) 336 | # print(inst) 337 | 338 | finally: 339 | print("vbit-iv clean up") 340 | -------------------------------------------------------------------------------- /packet.py: -------------------------------------------------------------------------------- 1 | # packet.py Teletext packet decoder 2 | # Takes a T42 packet and decodes whatever it can. 3 | # 4 | # Copyright (c) 2020, 2021 Peter Kwan 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | # 24 | # packet decoding is a bit over complicated. Let's put it all here so we know 25 | # where to look 26 | 27 | from clut import clut, Clut 28 | from mapper import getdiacritical, MapLatinG2 29 | 30 | # This doesn't want to be packets centred, it is pages meta data. 31 | # In other words: 32 | # Packets are analysed here. 33 | # Data decoded from the packets may be retrieved but not specific data about the packets themselves 34 | class Packet: 35 | def __init__(self): 36 | self.clear() 37 | return 38 | 39 | # reset all meta-data to the defaults 40 | def clear(self): 41 | self.region = 0 42 | self.X26CharMappings = [] # diacriticals 43 | # X28/0 settings 44 | self.ChangeColour = [] # background colour replacement 45 | self.RowColour = [] # tuple(row 0..24 , clut number 0..3, colour index 0..7) 46 | self.BlackBackgroundColourSubstitution = False 47 | self.ColourTableRemapping=0 # clut swap 48 | self.leftSidePanel = True # These should default to False for level 1 49 | self.rightSidePanel = True 50 | clut.reset() 51 | 52 | def mapColourFg(self, row, column, colour): 53 | return self.mapColour(row, column, colour, True) 54 | 55 | def mapColourBg(self, row, column, colour): 56 | return self.mapColour(row, column, colour, False) 57 | 58 | # X26/0 full colour row 59 | # @param row - Row number to check 60 | # @return - The colour value for that row, or black if there is none 61 | def rowColour(self, row): 62 | for i in self.RowColour: 63 | r = i[0] # row 64 | a = i[3] # if set, then all rows from here are coloured 65 | if r == row or ((row > r) and a) : # our row? 66 | c = i[1] # clut 67 | h = i[2] # index 68 | return clut.get_value(c, h) 69 | return '#000' # default row to black 70 | 71 | # If there is an X26/0 mapped colour, return it 72 | # @row - Row index of a spacing attribute 73 | # @column - Column index of a spacing attribute 74 | # @IsFg - True if the colour we are looking for is a foreground colour 75 | # @return The colour mapped at the address 76 | def mapColour(self, row, column, colour, isFg): 77 | # print("[packet::mapColour] entered. row = " + str(row) + " col = " + str(column) + " clr= " + str(colour) ) 78 | # look through the ChangeColour list to find if we have a matched address 79 | for i in self.ChangeColour: # [row, column, clutIx, colourIx, isFg] 80 | r = i[0] 81 | c = i[1] 82 | fg = i[4] 83 | #print ("[mapColour] r = " + str(r)) 84 | #print ("[mapColour] c = " + str(c)) 85 | #print ("[mapColour] i[0]" + str(i[0])) 86 | # If the address matches and the foreground or background type matches 87 | if (row - 1) == r and column == c and fg == isFg: 88 | #print('[mapColour] ' + str(i)) 89 | #print('[mapColour matched] row, col = ' + str(row) + ', ' + str(column) + ' fg = ' + str(isFg)) 90 | #print ("[mapColour matched] clut[ix] " + str(i[2]) + '[' + str(i[3])+ ']') 91 | colour = clut.get_value(i[2], i[3]) 92 | #print('[mapColour matched] colour = ' + colour) 93 | return colour # found and replaced 94 | return 'x' 95 | 96 | 97 | # decode a packet. Returns the packet type or 0 if it is not X/26, X28, X29 98 | # @param pkt - T42 packet 99 | # @param row - Packet number 100 | def decode(self, pkt, row): 101 | print("[Packet::decode] " + str(row) + " ************************************ Enters") 102 | dc = self.deham(pkt[2]) # designation code 103 | 104 | if (dc == 0 or dc == 4) and row == 28: 105 | #self.dumpPacket(pkt) # debug 106 | self.decodeX280Format1(pkt) 107 | return 108 | if row == 26: 109 | print("[decode] packet 26 dc = " + str(dc))#self.dumpPacket(pkt) # debug 110 | self.decodeX260(pkt) 111 | return 112 | 113 | print("[Packet::decode] Not implemented X/" + str(row) + "/ dc = " + str(dc)) 114 | 115 | print("[Packet::decode] *************************** exits") 116 | 117 | def decodeX280Format1(self, pkt): # X/28/0 format 1. p32 table 4 118 | # "Default G0 primary and G2 supplementary character sets plus national option character 119 | # sub-sets are designated. The 7-bit value is used to select an entry in table 32." 120 | global clut 121 | dc = self.deham(pkt[2]) # 0 = CLUT 2/3, 4 = CLUT 0/1 122 | print("[Packet::decodeX280Format1] Packet X/28/" + str(dc)+ " format 1") 123 | 124 | # @todo "Where packets 28/0 and 28/4 are both transmitted as part of a page, packet 28/0 takes precedence over 125 | # 28/4 for all but the colour map entry coding." 126 | 127 | triplets = self.decodeTriplets(pkt) # decode all the triplets 128 | # self.printTriplets(triplets) # debug 129 | 130 | # Decode everything and decide if we want to do anything with it later 131 | t = triplets[0] 132 | function = t & 0x0f # 133 | coding = (t > 4) & 0x07 # page coding (7 bits + parity) 134 | G0G2 = (t >> 8) & 0x7f # The lowest three bits are don't care 135 | 136 | self.region = (t >> 10) & 0x0f # This is the RE region number in tti files. 137 | 138 | print ("[Packet::decodeX280Format1] region = " + str(self.region)) 139 | secondG0 = ((t >> 14) & 0x0f) << 3 # 7 bit value defined in table 33. 140 | t = triplets[1] 141 | secondG0 = secondG0 | t & 0x07 142 | self.leftSidePanel = t & 0x08 > 0 143 | self.rightSidePanel = t & 0x10 > 0 144 | print("* coding = "+ str(coding) + " secondG0 = " + str(secondG0) + " leftPanel = " + str(self.leftSidePanel) + " rightPanel = " + str(self.rightSidePanel)) 145 | sidePanelStatus = t & 0x20 > 0 # Level 3.5 only 146 | sidePanelColumns = t & 0x1c >> 6 # Number of columns in side panels 147 | print("* sidePanelStatus = " + str(sidePanelStatus) + " sidePanelColumns = " + str(sidePanelColumns)) 148 | # the rest is colour, 16 lots of 4 bit RGB 149 | bit_index = 10 150 | triplet_start = 1 151 | colour = 0 152 | # for i in range(16 * 3): 153 | for i in range(16 * 3): # @todo Should be 16 for the two palettes 154 | # work out the indices 155 | start_bit = (i * 4) + bit_index 156 | triplet_index = triplet_start + int(start_bit / 18) 157 | start_bit = start_bit % 18 158 | colour_index = int(i/3) # CLUT 0/1 for dc == 4 159 | 160 | colour_value = i % 3 # RGB 161 | clut_ix = 1 #clut 0/1 where dc = 4 162 | if i < (8 * 3): 163 | clut_ix = 0 164 | if dc == 0: # CLUT 2/3 for dc == 0 165 | clut_ix = clut_ix + 2 166 | # extract the 4 bit colour value 167 | t = triplets[triplet_index] # Get the triplet 168 | #print("[decodeX280Format1] triplet = " + hex(t)) 169 | t = (t >> start_bit) & 0x0f # Shift and mask 170 | #print("[decodeX280Format1] masked = " + hex(t)) 171 | # does the data cross a triplet boundary? 172 | if start_bit > 14: 173 | split = 18 - start_bit # This is always 2! Could assert that 174 | t = t << split 175 | t2 = triplets[triplet_index+1] & 0x03 # Triplets only ever break on two bits 176 | t = t | t2 177 | 178 | # print("split = " + str(split)) 179 | 180 | 181 | #print("[decodeX280Format1] i = " + str(i) + " colour = " + str(colour_index) + "(" + str(colour_value) + ") triplet = " + str(triplet_index) + " start_bit = " + str(start_bit) + " Colour = " + hex(t)) 182 | colour = colour | t << ((2-colour_value) * 4) 183 | #print("[decodeX280Format1]"+hex(t)+" ",end='') 184 | if colour_value == 2: # Done an RGB value 185 | colourHex = '#' + f'{colour:03x}' # This is the colour in 12 bit web hex format 186 | clut.set_value(colourHex, clut_ix, colour_index) 187 | #print("[decodeX280Format1] colour = " + hex(colour) +", colourHex = " + colourHex ) 188 | colour = 0 189 | # remaining bits 190 | t = triplets[13-1] 191 | self.BlackBackgroundColourSubstitution = (t & 0x04000) > 0 # page 33. Bit 15 192 | self.ColourTableRemapping = (t >> 15) & 0x07 193 | print("[decodeX280Format1] EXITS") 194 | 195 | def decodeX260(self, pkt): 196 | print("[decodeX260] ENTERS") 197 | 198 | # There is a lot of stuff in X26, diacriticals, colours, side panels ... 199 | tp = self.decodeTriplets(pkt) 200 | dc = self.deham(pkt[2]) # 0 201 | 202 | print("[decodeX260] dc = " + str(dc)) 203 | for i in range (0, 13): 204 | x = tp[i] # self.getTriplet(i, pkt) 205 | data = (x >> 11) & 0x7f 206 | mode = (x >> 6) & 0x1f 207 | address = x & 0x3f 208 | if mode != 0x1f: # filter out termination marker 209 | print("[decodeX260] Packet 26 triplet = " + str(i) + " data = " + hex(data) + " address = " + str(address), end='') 210 | if address>=40 and address<=63: # It is a row address group. TABLE 27. 211 | modeStr = { 212 | 0x01: "Full row colour", 213 | 0x02: "Reserved", 214 | 0x03: "Reserved", 215 | 0x04: "Set Active Position", 216 | 0x05: "Reserved", 217 | 0x06: "Reserved", 218 | 0x07: "Address display row0", 219 | 0x08: "PDC Data - Country", 220 | 0x09: "PDC Data - Month and day", 221 | 0x0a: "PDC Data - Cursor row, start time", 222 | 0x0b: "PDC Data - Cursor row, end time", 223 | 0x0c: "PDC", 224 | 0x0d: "PDC", 225 | 0x0e: "Reserved", 226 | 0x0f: "Reserved", 227 | 0x12: "Adaptive Object Invocation", 228 | 0x19: "Reserved", 229 | 0x1a: "Reserved", 230 | 0x1b: "Reserved", 231 | 0x1c: "Reserved", 232 | 0x1d: "Reserved", 233 | 0x1e: "Reserved", 234 | 0x1f: "Termination marker", 235 | } 236 | if address>=0 and address<=39: # It is a column address group 237 | modeStr = { 238 | 0x00: "Foreground colour", 239 | 0x01: "Block mosaic character G1", 240 | 0x02: "Smoothed mosaic G3", 241 | 0x03: "Background colour", 242 | 0x04: "Reserved", 243 | 0x05: "Reserved", 244 | 0x06: "PDC - Cursor column", 245 | 0x07: "Additional flash functions", 246 | 0x08: "Modified G0/G2 character set", 247 | 0x09: "Character from G0 set (2.5, 3.5)", 248 | 0x0a: "Reserved", 249 | 0x0b: "Line drawing or smoothed mosaic G3 set (2.5, 3.5)", 250 | 0x0c: "Display attributes", 251 | 0x0d: "DRCS character invocation", 252 | 0x0e: "Font style", 253 | 0x0f: "Character from the G2 set", 254 | 0x10: "G0 character without diacritical mark", 255 | 0x11: "G0 character with diacritical mark", 256 | 0x12: "G0 character with diacritical mark", 257 | 0x13: "G0 character with diacritical mark", 258 | 0x14: "G0 character with diacritical mark", 259 | 0x15: "G0 character with diacritical mark", 260 | 0x16: "G0 character with diacritical mark", 261 | 0x17: "G0 character with diacritical mark", 262 | 0x18: "G0 character with diacritical mark", 263 | 0x19: "G0 character with diacritical mark", 264 | 0x1a: "G0 character with diacritical mark", 265 | 0x1b: "G0 character with diacritical mark", 266 | 0x1c: "G0 character with diacritical mark", 267 | 0x1d: "G0 character with diacritical mark", 268 | 0x1e: "G0 character with diacritical mark", 269 | 0x1f: "G0 character with diacritical mark", 270 | } 271 | if mode == 0x1f: # A termination marker, nothing more to come 272 | return 273 | print (" tuple("+str(i)+") mode(" + hex(mode) + ") = " + modeStr.get(mode, hex(mode))) 274 | if address>=40 and address<=63: # It is a row address group 275 | self.rowAddr = address - 40 276 | if mode == 0x01: # Full Row Colour. Page 83 277 | # print ('[decodeX260] todo Full row colour mode 0x01') 278 | # push a tuple(row, clut, colour) 279 | s = (data >> 5) & 0x03 # 0..3 280 | multipleRow = s != 0 281 | clutIndex = (data >> 3) & 0x03 # 0..3 282 | colourIndex= data & 0x07 # 0..7 283 | self.RowColour.append(tuple((self.rowAddr, clutIndex, colourIndex, multipleRow ))) 284 | # we can extract S: 0=row 3=area, C = Clut index, data = colour 285 | # We will need to save this away and execute it 286 | # @todo Modes between 0 and 0x1f 287 | #When data field D6 and D5 are both set to '0', bits D4 - D0 define the background 288 | #colour. Bits D4 and D3 select a CLUT in the Colour Map of table 30, and bits D2 - 289 | #D0 select an entry from that CLUT. All other data field values are reserved. 290 | #The effect of this attribute persists to the end of a display row unless overridden 291 | #by either a spacing or a non-spacing attribute defining the background colour. 292 | if mode == 0x04: # Set Active Position 293 | if data<40: # level >= 2.5 294 | self.colAddr = data 295 | # print("rowAddr = " + str(self.rowAddr)) 296 | if address>=0 and address<=39: # It is a column address group 297 | # @todo Modes between 0 and 0x1f 298 | if mode == 0x00: #Foreground colour 299 | if (data & 0x60) == 0: 300 | clutIndex = (data >> 3) & 0x03 # Which CLUT? 301 | colourIndex = data & 0x07 # Which colour in the CLUT? 302 | print ("[decodeX260] set foreground colour at (" + str(self.rowAddr) + ", " + str(address) + ") to " + str(clutIndex) + "[" + str(colourIndex) +']') 303 | fgCol = [self.rowAddr, address, clutIndex, colourIndex, True] 304 | self.ChangeColour.append(fgCol) 305 | if mode == 0x01: # Block Mosaic Character from the G1 Set. Page 90. Level 1.5 306 | # @todo Probably needs to take into account contiguous/separated 307 | ch = data & 0x7f 308 | if ch >= 0x20 and ch < 0x40: 309 | ch = ch + 0xe680 - 0x20 310 | if ch >= 0x60 and ch < 0x80: 311 | ch = ch + 0xe6a0 - 0x60 312 | mapChar = tuple((self.rowAddr, address, ch)) 313 | self.addMapping(mapChar) 314 | if mode == 0x02: # Smoothed Mosaic Character from the G3 Set. Page 90 315 | # See table 48, Page 127 316 | ch = data & 0x7f 317 | if ch >= 0x20 and ch < 0x60: 318 | ch = ch + 0xe700 - 0x20 319 | if ch >= 0x60 and ch < 0x80: 320 | ch = ch + 0xeee0 - 0x60 321 | mapChar = tuple((self.rowAddr, address, ch)) 322 | self.addMapping(mapChar) 323 | if mode == 0x03: # Background colour 324 | if (data & 0x60) == 0: 325 | clutIndex = (data >> 3) & 0x03 # Which CLUT? 326 | colourIndex = data & 0x07 # Which colour in the CLUT? 327 | print ("[decodeX260] set background colour at (" + str(self.rowAddr) + ", " + str(address) + ") to " + str(clutIndex) + "[" + str(colourIndex) +']') 328 | bgCol = [self.rowAddr, address, clutIndex, colourIndex, False] 329 | self.ChangeColour.append(bgCol) 330 | if mode == 0x09: # Character from G0 set (2.5, 3.5) 331 | #print('[decodeX260: mode 9] place character ' + hex(data) + " at (" + str(address) +", " + str(self.rowAddr) + ")") 332 | mapChar = tuple((self.rowAddr, address, data)) 333 | self.addMapping(mapChar) 334 | if mode == 0x0b: # Line Drawing and Smoothed Mosaic Character 335 | # from the G3 Set at Levels 2.5 and 3.5. Level 1.5 should ignore this 336 | ch = data & 0x7f 337 | if ch >= 0x20 and ch < 0x60: 338 | ch = ch + 0xe700 - 0x20 339 | if ch >= 0x60 and ch < 0x80: 340 | ch = ch + 0xeee0 - 0x60 341 | mapChar = tuple((self.rowAddr, address, ch)) 342 | self.addMapping(mapChar) 343 | if mode == 0x0c: # Display attributes 344 | print('[setLine] Display Attributes, data = ' + hex(data)) 345 | if data & 0x10: # invert colours 346 | ch = ch # @todo placeholder 347 | if mode == 0x0e: # Font style. P94 Table 29 348 | # Data bit 2=italic 1=bold 0=proprotional 349 | # Probably can do them as tkinter font attributes 350 | ch = ch # @todo 351 | if mode == 0x0f: # Character from the G2 Supplementary Set. Page 94 352 | g2char = MapLatinG2(data) 353 | mapChar = tuple((self.rowAddr, address, g2char)) 354 | self.addMapping(mapChar) 355 | if mode == 0x10: # Character from G0 set (2.5, 3.5) 356 | print('[decodeX260: mode 10] place character ' + hex(data) + " at (" + str(address) +", " + str(self.rowAddr) + ")") 357 | mapChar = tuple((self.rowAddr, address, ord('?'))) 358 | self.addMapping(mapChar) 359 | if mode > 0x10 and mode <= 0x1f: # G0 character with diacritical mark 360 | self.colAddr = address 361 | dia = int(mode & 0x0f) 362 | # mapChar = tuple((self.rowAddr, self.colAddr, dia)) 363 | mapChar = tuple((self.rowAddr, self.colAddr, ord(getdiacritical(chr(data), dia)) )) 364 | # need to re-implement this 365 | self.addMapping(mapChar) 366 | print("[mapChar] = " + str(mapChar[0]) + " " + str(mapChar[1]) + " " + str(mapChar[2]) + " ") 367 | 368 | 369 | # Really need to make a better version of deham 370 | # @param 8 bit number to deham 371 | # @return 4 bit dehammed alue 372 | def deham(self, value): 373 | # Deham with NO checking! @todo Parity and error correction 374 | b0 = (value & 0x02) >> 1 375 | b1 = (value & 0x08) >> 2 376 | b2 = (value & 0x20) >> 3 377 | b3 = (value & 0x80) >> 4 378 | return b0+b1+b2+b3 379 | 380 | def decodeTriplet(self, b1, b2, b3): # ETS: Page 22, section 8.3 381 | #b1 = self.reverse(b1) 382 | #b2 = self.reverse(b2) 383 | #b3 = self.reverse(b3) 384 | # Don't care about errors. Just remove the hamming bits 385 | c1 = (b1 & 0x04) >> 2 | (b1 & 0x70) >> 3 # .XXX.X.. 386 | c2 = (b2 & 0x7f) << (8-4) # 4..10 387 | c3 = (b3 & 0x7f) << (16-5) # 11..17 388 | 389 | result = c1 | c2 | c3 390 | #print ("b1 =" + hex(b1) + " b2 =" + hex(b2) + " b3 =" + hex(b3)) 391 | #sprint ("c1 =" + hex(c1) + " c2 =" + hex(c2) + " c3 =" + hex(c3) + " " + hex(result)) 392 | return result 393 | 394 | # decode triplet in X/26 etc. 395 | # \param ix Triplet number 0 to 12 396 | def getTriplet(self, ix, pkt): 397 | i = (ix * 3) + 3 398 | return self.decodeTriplet(pkt[i] & 0x7f, pkt[i+1] & 0x7f, pkt[i+2] & 0x7f) 399 | 400 | # debug 401 | def printTriplets(self, tr): 402 | print("[debug] Triplet = ", end = '') 403 | for t in range(13): 404 | print(hex(tr[t]) + ' ', end = '') 405 | print() 406 | 407 | # @param pkt - An XX26/28.29 packet 408 | # @return - Array of 13 numbers, the decoded triplets 409 | def decodeTriplets(self, pkt): 410 | arr = [] 411 | for i in range(13): 412 | arr.append( self.getTriplet(i, pkt) ) 413 | return arr 414 | 415 | # dump a packet as hex 416 | def dumpPacket(self, pkt): 417 | # print('Dump: ' + f'{pkt[0]:02x}' + ' ' + f'{pkt[1]:02x}') 418 | print([f'{pkt[i]:02x}' for i in range(0, 42)]) 419 | 420 | # dump ALL the metadata in one handy chunk 421 | def dump(self): 422 | print("[Dump] Region = " + str(self.region)) 423 | clut.dump() 424 | print("[Dump] diacritical count = " + str(len(self.X26CharMappings))) 425 | print("[Dump] ColourTableRemapping = " + str(self.ColourTableRemapping)) # Table 33 426 | 427 | # The clut is shared 428 | # Other metadata has access functions 429 | 430 | # The region code number 431 | def getRegion(self): 432 | return self.region 433 | 434 | ############## X/26 character mappings ############### 435 | def addMapping(self, mappedChar): 436 | #print('TRACE A') 437 | self.X26CharMappings.append(mappedChar) 438 | 439 | 440 | metaData = Packet() -------------------------------------------------------------------------------- /ttxline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Teletext Stream to Invision decoder 4 | # 5 | # Copyright (c) 2020-2021 Peter Kwan 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | from tkinter import Text, END, NORMAL, DISABLED 26 | from tkinter.font import Font 27 | from mapper import mapchar, mapdiacritical 28 | from clut import clut, Clut 29 | from packet import Packet, metaData 30 | 31 | class TTXline: 32 | print("TTXLine created") 33 | 34 | # Map a teletext colour number to an actual colour 35 | # The CLUT needs to be chosen according to X26 settings so this isn't good enough 36 | # Probably also needs to go to CLUT instead of being in here 37 | def getcolour(self, c): 38 | global clut 39 | return clut.clut0[c] 40 | 41 | def __init__(self, root_param, height=360): 42 | # this is where we define a Text object and set it up 43 | self.root = root_param 44 | self.text = Text(self.root) 45 | 46 | # establish the maximum font size required to fill the available space 47 | # what is the window height? 48 | self.width_value=self.root.winfo_screenwidth() 49 | self.height_value=height 50 | 51 | # @todo Temporary hack to cope with my own multi-screen setup 52 | if self.width_value < self.height_value: 53 | self.height_value = self.width_value / 1.77 54 | 55 | lines = 25 56 | 57 | self.fontH=-round((-1+self.height_value/(lines+2)))# 58 | # self.ttxfont0 = Font(family='teletext2', size=round(self.fontH/2)) 59 | self.ttxfont2 = Font(family='teletext2', size=round(self.fontH)) 60 | self.ttxfont4 = Font(family='teletext4', size=round(self.fontH*2)) 61 | 62 | # allow for side panel of up to 16 characters 63 | side=16 64 | self.text = Text(self.root, width = 40+side, height = lines) # The normal text 65 | self.textConceal = Text(self.root, width = 40+side, height = lines) # Copy of text but with reveals hidden 66 | 67 | # Most of these options are failed attempts to remove the single pixel lines 68 | self.text.config(borderwidth=0, foreground='white', background='black', font=self.ttxfont2, padx=0, pady=0, autoseparators=0, highlightbackground='black', spacing1=0, spacing2=0, spacing3=-1) 69 | self.textConceal.config(borderwidth=0, foreground='white', background='black', font=self.ttxfont2, padx=0, pady=0, autoseparators=0, highlightbackground='black') 70 | 71 | for i in range(24): 72 | if i==11: 73 | self.text.insert(END, " VBIT IN-VISION \n") 74 | self.textConceal.insert(END, " VBIT IN-VISION \n") 75 | tag_id = "dbl" 76 | self.text.tag_add(tag_id, "12.0", '12.end') 77 | self.text.tag_config(tag_id, font=self.ttxfont4, offset=0, foreground = 'orange') # double height 78 | else: 79 | self.text.insert(END, " \n") 80 | self.textConceal.insert(END, " \n") 81 | #self.text.tag_add("all", "1.0", END) # test to delete 82 | #self.text.tag_config("all", spacing2 = 10) # test to delete 83 | 84 | self.rowOffset = 0 # Used to elide double height lines 85 | 86 | self.pageLoaded = False 87 | self.found = False 88 | self.currentHeader = bytearray() 89 | self.currentHeader.extend(b'YZ0123456789012345678901234567890123456789') # header of the page that is being displayed 90 | 91 | self.revealMode = False # hidden 92 | 93 | # header flags 94 | self.natOpt = 0 # 0=EN, 1=FR, 2=SW/FI/HU, 3=CZ/SK, 4=DE, 5=PT/SP, 6=IT, 7=N/A 95 | # self.region = 0 # National option selection bits in X/28/0 format 1. Used by RE in tti files. 96 | 97 | self.clearFlag = False # Set by clear(), cleared by printHeader() 98 | 99 | self.offsetSplit = 8 # Where the side panels are split (0..16, default 8) 100 | 101 | def deham(self, value): 102 | # Deham with NO checking! @todo Parity and error correction 103 | b0 = (value & 0x02) >> 1 104 | b1 = (value & 0x08) >> 2 105 | b2 = (value & 0x20) >> 3 106 | b3 = (value & 0x80) >> 4 107 | return b0+b1+b2+b3 108 | 109 | # true if while in graphics mode, it is a mosaic character. False if control or upper case alpha 110 | def isMosaic(self, ch): 111 | return ch & 0x20; # Bit 6 set? 112 | 113 | def dump(self, pkt): 114 | return 115 | print("Dumping row") 116 | for i in range(len(pkt)): 117 | print(str(i)+' '+pkt.hex()) 118 | 119 | # clear and replace the line contents 120 | # @param packet : packet to write 121 | # @param row : row number to write (starting from 0) 122 | def setLine(self, pkt, row): 123 | #print('[setLine] ENTERS') 124 | # It has two phases 125 | # 1) Place all the characters on the line 126 | # 2) Set their attributes: colour and font size 127 | if row==2: 128 | self.dump(pkt) 129 | 130 | 131 | rstr = str(row + 1) + "." # The row string. First Text row is 1 132 | tag_start=str(rstr +"0") 133 | tag_end=str(rstr +"end") 134 | 135 | # wsfn remove this for testing 136 | for tag in self.text.tag_names(): # erase the line attributes 137 | attr = tag.split('-') 138 | if attr[0] == str(row+1): 139 | #print('deleting tag = ' + tag) 140 | self.text.tag_delete(tag) 141 | self.textConceal.tag_delete(tag) 142 | #print ("[setLine]row = "+str(row)) 143 | # erase the line 144 | self.text.delete(tag_start, tag_end ) # erase the line 145 | self.textConceal.delete(tag_start, tag_end ) 146 | 147 | 148 | # Set the conditions at the start of the line 149 | graphicsMode = False 150 | hasDoubleHeight = False 151 | holdChar = 0x00 152 | holdMode = False 153 | contiguous = True 154 | concealed = False 155 | flashMode = False # @todo 156 | 157 | lastMosaicChar = ' ' 158 | 159 | self.text.insert(tag_start, " ") # This could be a big mistake 160 | self.text.insert(tag_start, " ") 161 | 162 | # PASS 1: put the characters in. Selects glyphs for alpha, contiguous gfx, separated gxf 163 | #print('[setLine] rendering row ' + str(row)) 164 | for i in range(40): 165 | c = pkt[i+2] & 0x7f # strip parity 166 | # Convert control code ascii 167 | # @todo Regional mappings 168 | ch = chr(c) 169 | if c < 0x08 or c >= 0x10 and c < 0x18: # colour codes cancel conceal mode 170 | concealed = False 171 | #if c == 0x0f: # double size 172 | # print("double size not implemented") # Not the same as double height 173 | if c == 0x1e: # hold graphics - set at 174 | holdMode = True 175 | holdChar = lastMosaicChar # ' ' 176 | if c == 0x18: # conceal mode - set at 177 | concealed = True 178 | if c == 0x19: # Contiguous graphics 179 | contiguous = True 180 | if c == 0x1a: # Separated graphics 181 | contiguous = False 182 | if graphicsMode: 183 | # If it is a mosaic, work out what character it is 184 | if self.isMosaic(c): 185 | if contiguous: 186 | ch = chr(c + 0x0e680 - 0x20) # contiguous 187 | else: 188 | ch = chr(c + 0x0e680) # separate 189 | if holdMode: 190 | holdChar = ch # save the character for later 191 | lastMosaicChar = ch 192 | else: 193 | if ch<' ': # Unprintable? 194 | if holdMode: 195 | ch = holdChar # non printable and in hold 196 | else: 197 | ch = ' ' # Non printable 198 | else: 199 | ch = mapchar(ch, self.natOpt , metaData.getRegion()) # text in alpha mode @todo implement group number 200 | ch = mapdiacritical(ch, row, i, metaData.X26CharMappings) 201 | # if it is not a mosaic and we are in hold mode, substitute the character 202 | else: 203 | # alpha is way simpler 204 | if ch < ' ': 205 | ch = ' ' 206 | else: 207 | ch = mapchar(ch, self.natOpt , metaData.getRegion()) # text in alpha mode @todo implement group number 208 | ch = mapdiacritical(ch, row, i, metaData.X26CharMappings) 209 | # Add the character, unless it is hidden 210 | self.text.insert(rstr+str(i+self.offsetSplit), ch if not concealed else ' ') 211 | # Keep the concealed characters only 212 | self.textConceal.insert(rstr+str(i+self.offsetSplit), ch if concealed else ' ') # Save the characters that ARE concealed 213 | # set-after 214 | if c == 0x1f: # release graphics - set after 215 | holdMode = False 216 | if c < 0x08: # alpha colours 217 | graphicsMode = False 218 | if c >= 0x10 and c < 0x18: # Mosaic colour 219 | graphicsMode = True 220 | 221 | # PASS 2: Add text attributes: font, colour, flash 222 | 223 | # Any full row colours? 224 | #print('[setLine] rendering pass 2') 225 | foreground_colour = 7 # 'white' 226 | background_colour = 0 # 'black' 227 | text_height = 'single' 228 | 229 | # Set the initial colour for the row 230 | # background_colour = metaData.rowColour(row) # X26/0 full row colour triplet 231 | #print('[setLine] row = ' + str(row) + " bgcol = " + background_colour) 232 | 233 | # Complicate things if side panels are enabled 234 | if metaData.leftSidePanel or metaData.rightSidePanel: 235 | #print('[setLine] SETTING SIDE PANELS') 236 | tag_id = "rowBGCol"+str(row) 237 | fg = clut.RemapColourTable(foreground_colour, metaData.ColourTableRemapping, True) 238 | bg = metaData.rowColour(row) 239 | self.text.tag_config(tag_id , font = self.ttxfont2, foreground = fg, background = bg) 240 | self.textConceal.tag_config(tag_id , font = self.ttxfont2, foreground = fg, background = bg) 241 | if metaData.BlackBackgroundColourSubstitution: 242 | self.text.tag_add(tag_id, rstr + str(0), rstr + 'end') # whole row 243 | self.textConceal.tag_add(tag_id, rstr + str(0), rstr + 'end') # whole row 244 | else: 245 | # Side panels only 246 | # @ todo Check where the actual split is, not just 8+8 247 | background_colour = 0 # 'black' 248 | if metaData.leftSidePanel: 249 | self.text.tag_add(tag_id, rstr + str(0), rstr + str(8)) # left panel 250 | self.textConceal.tag_add(tag_id, rstr + str(0), rstr + str(8)) # left panel 251 | if metaData.rightSidePanel: 252 | self.text.tag_add(tag_id, rstr + str(48), rstr + 'end') # right panel 253 | self.textConceal.tag_add(tag_id, rstr + str(48), rstr + 'end') # right panel 254 | 255 | 256 | # Set the text attributes: colour and font size 257 | # row 258 | row = str(row + 1) 259 | ix= 0 260 | attr = text_height 261 | for i in range(40): 262 | c = pkt[i+2] & 0x7f 263 | ch = chr(c) 264 | #if i==1 and c<0x20: 265 | #print (hex(c)) 266 | attributeChanged =False 267 | if c == 0x0c: # normal height 268 | # This code breaks if there is a normal size but NO double height on the line 269 | text_height = 'single' 270 | #tag_id = "thc"+str(row)+"-"+str(i) 271 | attributeChanged = True 272 | #tag_id = text_height + '-' + foreground_colour + '-' + background_colour 273 | #self.text.tag_add(tag_id, rstr + str(i+1), rstr + 'end') # column + 1 - set-after 274 | # self.text.tag_config(tag_id, font=self.ttxfont2, offset=0) # normal height too 275 | # @todo Doing another pass for the offset is the only way to make it work correctly, probably 276 | if c == 0x0d: # double height 277 | text_height = 'double' 278 | hasDoubleHeight = True 279 | attributeChanged = True 280 | #tag_id = "thd"+str(row)+"-"+str(i) 281 | #tag_id = text_height + '-' + foreground_colour + '-' + background_colour 282 | #self.text.tag_add(tag_id, rstr + str(i+1), rstr + 'end') # column + 1 - set-after 283 | #self.text.tag_config(tag_id, font=self.ttxfont4, offset=0) # double height 284 | 285 | set_at = 1 # 0 = set at, 1 = set after 286 | if c==0x1c: # black background - set at 287 | background_colour = 0x00 288 | attributeChanged = True 289 | set_at = 0 290 | if c==0x1d: # new background - set at 291 | background_colour = foreground_colour 292 | attributeChanged = True 293 | set_at = 0 294 | if c < 0x08 : # alpha colour - set after 295 | foreground_colour = c 296 | attributeChanged = True 297 | # also need to see if there is an X26/0 background colour change 298 | #foreground_colour = metaData.mapColourFg(int(row), i, foreground_colour) 299 | #background_colour = metaData.mapColourBg(int(row), i, background_colour) 300 | if c >= 0x10 and c < 0x18: # Mosaic colour - set after 301 | foreground_colour = c-0x10 # self.getcolour(c-0x10) 302 | attributeChanged = True 303 | #foreground_colour = metaData.mapColourFg(int(row), i, foreground_colour) 304 | #background_colour = metaData.mapColourBg(int(row), i, background_colour) 305 | # also need to see if there is an X26/0 foreground colour change 306 | fg = metaData.mapColourFg(int(row), i+1, foreground_colour) # Set after 307 | if fg != 'x': 308 | attributeChanged = True 309 | #foreground_colour = fg 310 | bg = metaData.mapColourBg(int(row), i+1, background_colour) # Set at 311 | if bg != 'x': 312 | attributeChanged = True 313 | #background_colour = bg 314 | if attributeChanged: 315 | if fg != 'x': 316 | fgc = fg 317 | else: 318 | fgc = clut.RemapColourTable(foreground_colour, metaData.ColourTableRemapping, True) 319 | if bg != 'x': 320 | bgc = bg 321 | else: 322 | bgc = clut.RemapColourTable(background_colour, metaData.ColourTableRemapping, False) 323 | # tag_id identifies the row 324 | tag_id = row + '-' + str(ix) + '-' + text_height + '-' + fgc + '-' + str(background_colour) 325 | ix = ix + 1 326 | self.text.tag_add(tag_id, rstr + str(i+set_at+self.offsetSplit), rstr + str(48)) # @todo This depends on the side panel columns 327 | self.textConceal.tag_add(tag_id, rstr + str(i+set_at+self.offsetSplit), rstr + str(48)) # 328 | 329 | if text_height == 'double': 330 | textFont = self.ttxfont4 331 | #print("line 325. It got here") 332 | else: 333 | textFont = self.ttxfont2 334 | self.text.tag_config(tag_id , font = textFont, foreground = fgc, background = bgc) 335 | self.textConceal.tag_config(tag_id , font = textFont, foreground = fgc, background = bgc) 336 | 337 | self.text.config(state = DISABLED) # prevent editing 338 | return hasDoubleHeight 339 | 340 | def decodeFlags(self, packet): 341 | return 342 | flags = [0,0,0,0,0,0,0,0,0] 343 | for i in range(8): 344 | flags[i] = self.deham(packet[i+2]) 345 | print (hex(flags[i]) + ', ', end='') 346 | print() 347 | page = flags[1]*0x10 + flags[0] 348 | C4 = (flags[3] & 0x08) > 0 # clear 349 | C5 = (flags[5] & 0x04) > 0 # newsflash 350 | self.natOpt = (flags[7] >> 1) & 0x07 # C12, C13, C14 351 | C11 = flags[7] & 0x01 # Serial tx 352 | print("Page = " + hex(page) + ", C4(clear) = " + str(C4) + ", C5 = " + str(C5) + " natOpt = " + str(self.natOpt)) 353 | 354 | # param page - An 8 character info string for the start of the header 355 | def printHeader(self, packet, page = "Header..", seeking = False, suppressHeader = False): 356 | if self.clearFlag: 357 | self.clearFlag = False 358 | lines = self.text.index(END) 359 | line = lines.split('.')[0] 360 | if int(line)>26: 361 | print("[printHeader] " + str(line) + " Too many lines. Some bug somewhere!") 362 | self.text.config(state = NORMAL) # allow editing 363 | buf = bytearray(packet) # convert to bytearray so we can modify it 364 | if suppressHeader: 365 | self.clear 366 | for i in range(42): # blank out the header bytes 367 | buf[i]=0x00 368 | print("SUPPRESS HEADER!") 369 | buf[10]=ord('x') 370 | buf[11]=ord('y') 371 | self.setLine(buf,0) 372 | return 373 | for i in range(34,42): # copy the clock 374 | self.currentHeader[i] = buf[i] 375 | #print(str(type(self.currentHeader))) 376 | #print(str(type(buf))) 377 | 378 | for i in range(10): # blank out the header bytes 379 | buf[i]=buf[i] & 0x7f 380 | if buf[i]<0x20: 381 | buf[i]=0x20 382 | for i in range(2,10): 383 | buf[i]=ord(page[i-2]) 384 | 385 | if seeking: 386 | #self.pageLoaded = False 387 | self.currentHeader = buf # The whole header is updating 388 | self.found = False 389 | else: 390 | if not self.found: 391 | #print("[ttxline::printHeader] Calling clear") 392 | #self.clear("new header") 393 | self.currentHeader = buf # The whole header is updating 394 | self.found = True 395 | self.revealMode = False # New page starts with concealed text 396 | # Now that we have found the page, dump all of the tags 397 | # @todo Probably change this to tag_remove 398 | # for tag in self.text.tag_names(): # This clears all tags BUT only when moving to a new page 399 | # self.text.tag_delete(tag) 400 | self.decodeFlags(packet) 401 | 402 | #if not self.pageLoaded: 403 | # self.pageLoaded = True 404 | buf = self.currentHeader # The header stays on the loaded page 405 | 406 | self.setLine(buf, 0) 407 | 408 | # Now that the buffer has the correct characters loaded, we can set the generated page number 409 | #@todo Change the colour of the page number while seeking a page 410 | #self.text.delete("1.0", "1.8") # strip the control bytes 411 | #self.text.insert("1.0", "pagexxxx") # add the page number 412 | #print("inserting <" + page +'>') 413 | # self.text.insert("1.4", " ") # pad the remaining space 414 | #self.text.tag_add("pageColour", "1.0", "1.7") 415 | if seeking or page[0] == 'H': # Page number goes green in HOLD or while seeking 416 | self.text.tag_add("pagenumber", "1.0", "1.7") 417 | self.text.tag_config("pagenumber", foreground = "green1") # seeking 418 | self.textConceal.tag_add("pagenumberc", "1.0", "1.7") 419 | self.textConceal.tag_config("pagenumberc", foreground = "green1") # seeking 420 | else: 421 | self.text.tag_add("pagenumber", "1.0", "1.7") 422 | self.text.tag_config("pagenumber", foreground = "white") # found 423 | self.textConceal.tag_add("pagenumberc", "1.0", "1.7") 424 | self.textConceal.tag_config("pagenumberc", foreground = "white") # found 425 | 426 | self.rowOffset = 0 427 | self.text.config(state = DISABLED) 428 | 429 | # Return True if the row includes double height 430 | def printRow(self, packet, row): 431 | self.text.config(state = NORMAL) # allow editing 432 | # If the line is double height, then skip the next line 433 | if self.setLine(packet, row - self.rowOffset): 434 | self.rowOffset=self.rowOffset+1 435 | return True 436 | return False 437 | 438 | # show/hide concealed text 439 | def toggleReveal(self): 440 | self.revealMode = not self.revealMode 441 | self.text.config(state = NORMAL) # allow editing 442 | for row in range(24): 443 | for col in range(40): 444 | p0 = str(row + 1) + '.' + str(col) 445 | ch = self.textConceal.get(p0) # The revealed character 446 | if ch!=' ': # It might be concealed 447 | if not self.revealMode: 448 | ch = ' ' # or it could be hidden 449 | p1 = str(row + 1) + '.' + str(col+1) 450 | self.text.insert(p0, ch) 451 | self.text.delete(p1) 452 | self.text.config(state = DISABLED) 453 | 454 | # Clear stuff including all the page modifiers 455 | def clear(self, reason): 456 | self.clearFlag = True 457 | # self.region = 0 458 | metaData.clear() 459 | # I think I want to clear all the rows, but this breaks it 460 | 461 | s = self.text.get('1.0', 'end') 462 | print("deleting lines. char count = "+str(len(s)) + " reason = " + reason) 463 | self.text.delete('1.0', 'end') 464 | self.textConceal.delete('1.0', 'end') 465 | # not sure this will work with side panels 466 | for row in range(1,24): 467 | self.text.insert(END, " \n") # 42 characters 468 | self.textConceal.insert(END, " \n") 469 | 470 | # self.text.delete('1.0') 471 | #str = " " 472 | #str2 = str.encode(str) 473 | 474 | #self.setLine(str, 4) 475 | #self.setLine(b'0123456789012345678901234567890123456789\n', 4) 476 | -------------------------------------------------------------------------------- /mapper.py: -------------------------------------------------------------------------------- 1 | # Mapper 2 | # 3 | # Copyright (c),2020 Peter Kwan 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # 24 | # Maps characters to according to region and group to the glyph in teletext2.ttf 25 | 26 | # \param c : character to map 27 | # \param option : national option number 0..7 28 | # \param region : region number 29 | def mapchar(c, option, region): 30 | if region==0: # West Europe 31 | return mapregion0(c, option) 32 | if region==1: # West Europe plus Polish 33 | return mapregion1(c, option) 34 | if region==2: # West Europe plus Turkish 35 | return mapregion2(c, option) 36 | if region==3: # East Europe 37 | return mapregion3(c, option) 38 | if region==4: # Russia 39 | return mapregion4(c, option) 40 | if region==6: # Russia 41 | return mapregion6(c, option) # turk + greek 42 | if region==8: # Arabic 43 | return mapregion8(c, option) 44 | if region==10: # Arabic @todo G0/G2 45 | return mapregion10(c, option) 46 | print("Unimplemented region code " + str(region)) 47 | return '¬' 48 | 49 | def mapregion0(c, option): # West Europe 50 | if option==0: 51 | return mapEN(c) 52 | if option==1: 53 | return mapFR(c) 54 | if option==2: 55 | return mapSE(c) 56 | if option==3: 57 | return mapCZ(c) 58 | if option==4: 59 | return mapDE(c) 60 | if option==5: 61 | return mapES(c) 62 | if option==6: 63 | return mapIT(c) 64 | if option==7: 65 | return mapEN(c) # spare 66 | print("Unknown language region 0, nat. option = " + str(option)) 67 | return mapEN(c) 68 | 69 | def mapregion1(c, option): # West Europe 70 | if option==0: 71 | return mapPL(c) 72 | if option==1: 73 | return mapFR(c) 74 | if option==2: 75 | return mapSE(c) 76 | if option==3: 77 | return mapCZ(c) 78 | if option==4: 79 | return mapDE(c) 80 | if option==5: 81 | return mapPL(c) # spare 82 | if option==6: 83 | return mapIT(c) 84 | if option==7: 85 | return mapPL(c) # spare 86 | print("Unknown language region 1, nat. option = " + str(option)) 87 | return mapEN(c) 88 | 89 | def mapregion2(c, option): # West Europe plus turkish 90 | if option==0: 91 | return mapEN(c) 92 | if option==1: 93 | return mapFR(c) 94 | if option==2: 95 | return mapSE(c) 96 | if option==3: 97 | return mapTR(c) # Turkish 98 | if option==4: 99 | return mapDE(c) 100 | if option==5: 101 | return mapES(c) 102 | if option==6: 103 | return mapIT(c) 104 | if option==7: 105 | return mapEN(c) # spare 106 | print("Unknown language region 2, nat. option = " + str(option)) 107 | return mapEN(c) 108 | 109 | def mapregion3(c, option): # East Europe 110 | if option==0: 111 | return mapEN(c) # spare 112 | if option==1: 113 | return mapFR(c) # spare 114 | if option==2: 115 | return mapSE(c) # spare 116 | if option==3: 117 | return mapTR(c) # spare 118 | if option==4: 119 | return mapDE(c) # spare 120 | if option==5: 121 | return mapRS(c) # serbian/croatian/slovenian 122 | if option==6: 123 | return mapEN(c) # spare 124 | if option==7: 125 | return mapRO(c) # romanian 126 | print("Unknown language region 3, nat. option = " + str(option)) 127 | return mapEN(c) 128 | 129 | def mapregion4(c, option): # Russia/Bulgaria 130 | print("Region 4 option = " + str(option)) 131 | if option==0: 132 | return mapRU(c) # serbian 133 | return mapRS(c) # serbian 134 | if option==1: 135 | return mapRU(c) # russian 136 | if option==2: 137 | return mapEE(c) # estonian (Same as czech/slovak) 138 | if option==3: 139 | return mapCZ(c) # czechia/slovak 140 | if option==4: 141 | return mapDE(c) # german 142 | if option==5: 143 | return mapUA(c) # ukrainian 144 | if option==6: 145 | return mapLV(c) # lettish(latvian)/lithuanian (latin) 146 | if option==7: 147 | return mapEN(c) # spare 148 | print("Unknown language region 4, nat. option = " + str(option)) 149 | return mapEN(c) 150 | 151 | def mapregion6(c, option): # Turkish-3/Greek-7 152 | if option==3: 153 | return mapTR(c) # turkish 154 | if option==7: 155 | return mapGR(c) # greek 156 | print("Unknown language region 6, nat. option = " + str(option)) 157 | return mapEN(c) 158 | 159 | def mapregion8(c, option): # Arabic 160 | if option==0: 161 | return mapEN(c) # english 162 | if option==1: 163 | return mapFR(c) # franch 164 | if option==7: 165 | return mapAR(c) # arabic 166 | print("Unknown language region 8, nat. option = " + str(option)) 167 | return mapEN(c) 168 | 169 | def mapregion10(c, option): # Arabic 170 | if option==5: 171 | return mapHE(c) # hebrew 172 | if option==7: 173 | return mapAR(c) # arabic 174 | print("Unknown language region 10, nat. option = " + str(option)) 175 | return mapEN(c) 176 | 177 | def mapEN(c): # English 178 | mapper = { 179 | '#': '£', 180 | '[': chr(0x2190), # 5/B Left arrow. 181 | '\\':chr(0xbd), # 5/C Half 182 | ']': chr(0x2192), # 5/D Right arrow. 183 | '^': chr(0x2191), # 5/E Up arrow. 184 | '_': chr(0x0023), # 5/F Underscore is hash sign 185 | '`': chr(0x2014), # 6/0 Centre dash. The full width dash e731 186 | '{': chr(0xbc), # 7/B Quarter 187 | '|': chr(0x2016), # 7/C Double pipe 188 | '}': chr(0xbe), # 7/D Three quarters 189 | '~': chr(0x00f7), # 7/E Divide 190 | chr(0x7f): chr(0xe65f) # 7/F Bullet (rectangle block) 191 | } 192 | return mapper.get(c, c) 193 | 194 | def mapFR(c): # French Nat. Opt. 1 195 | mapper = { 196 | '#': chr(0x00e9), # 2/3 e acute 197 | '$': chr(0x00ef), # 2/4 i umlaut 198 | '@': chr(0x00e0), # 4/0 a grave 199 | '[': chr(0x00eb), # 5/B e umlaut 200 | '\\': chr(0x00ea), # 5/C e circumflex 201 | # Nat. opt. 2 202 | ']': chr(0x00f9), # 5/D u grave 203 | '^': chr(0x00ee), # 5/E i circumflex 204 | '_': '#', # 5/F # 205 | '`': chr(0x00e8), # 6/0 e grave 206 | '{': chr(0x00e2), # 7/B a circumflex 207 | '|': chr(0x00f4), # 7/C o circumflex 208 | '}': chr(0x00fb), # 7/D u circumflex 209 | '~': chr(0x00e7), # 7/E c cedilla 210 | } 211 | return mapper.get(c, c) 212 | 213 | def mapSE(c): # Swedish Nat. Opt. 2, group 0 214 | mapper = { 215 | # '#': '#', # 2/3 hash not mapped 216 | '$': chr(0x00a4), # 2/4 currency bug 217 | '@': chr(0x00c9), # 4/0 E acute 218 | '[': chr(0x00c4), # 5/B A diaresis 219 | '\\': chr(0x00d6), # 5/C O diaresis 220 | # Nat. opt. 2 221 | ']': chr(0x00c5), # 5/D A ring 222 | '^': chr(0x00dc), # 5/E U diaresis 223 | '_': chr(0x005f), # 5/F Underscore (not mapped) 224 | '`': chr(0x00e9), # 6/0 e acute 225 | '{': chr(0x00e4), # 7/B a diaresis 226 | '|': chr(0x00f6), # 7/C o diaresis 227 | '}': chr(0x00e5), # 7/D a ring 228 | '~': chr(0x00fc), # 7/E u diaresis 229 | } 230 | return mapper.get(c, c) 231 | 232 | def mapCZ(c): # Czech/Slovak Nat. Opt. 3, group 0 233 | mapper = { 234 | # '#': '#' # 2/3 hash 235 | '$': chr(0x016f),# 2/4 u ring 236 | '@': chr(0x010d),# 4/0 c caron 237 | '[': chr(0x0165),# 5/B t caron 238 | '\\': chr(0x017e),# 5/C z caron 239 | # Nat. opt. 2 240 | ']': chr(0x00fd),# 5/D y acute 241 | '^': chr(0x00ed),# 5/E i acute 242 | '_': chr(0x0159),# 5/F r caron 243 | '`': chr(0x00e9),# 6/0 e acute 244 | '{': chr(0x00e1),# 7/B a acute 245 | '|': chr(0x011b),# 7/C e caron 246 | '}': chr(0x00fa),# 7/D u acute 247 | '~': chr(0x0161)# 7/E s caron 248 | } 249 | return mapper.get(c, c) 250 | 251 | 252 | def mapDE(c): # German Nat. Opt. 4, group 0 253 | mapper = { 254 | # '#': '#' # 2/3 # is not mapped 255 | '$': chr(0x0024),# 2/4 Dollar sign not mapped 256 | '@': chr(0x00a7),# 4/0 Section sign 257 | '[': chr(0x00c4),# 5/B A umlaut 258 | '\\': chr(0x00d6),# 5/C O umlaut 259 | # Nat. opt. 2 260 | ']': chr(0x00dc),# 5/D U umlaut 261 | #'^': '^',# 5/E Caret (not mapped) 262 | '_': chr(0x005f),# 5/F Underscore (not mapped) 263 | '`': chr(0x00b0),# 6/0 Masculine ordinal indicator 264 | '{': chr(0x00e4),# 7/B a umlaut 265 | '|': chr(0x00f6),# 7/C o umlaut 266 | '}': chr(0x00fc),# 7/D u umlaut 267 | '~': chr(0x00df)# 7/E SS 268 | } 269 | return mapper.get(c, c) 270 | 271 | def mapES(c): # Spanish/Portuguese Nat. Opt. 5, group 0 272 | mapper = { 273 | '#': chr(0x00e7),# 2/3 c cedilla 274 | # '$': '$' # 2/4 Dollar sign not mapped 275 | '@': chr(0x00a1),# 4/0 inverted exclamation mark 276 | '[': chr(0x00e1),# 5/B a acute 277 | '\\': chr(0x00e9),# 5/C e acute 278 | # Nat. opt. 2 279 | ']': chr(0x00ed),# 5/D i acute 280 | '^': chr(0x00f3),# 5/E o acute 281 | '_': chr(0x00fa),# 5/F u acute 282 | '`': chr(0x00bf),# 6/0 Inverted question mark 283 | '{': chr(0x00fc),# 7/B u umlaut 284 | '|': chr(0x00f1),# 7/C n tilde 285 | '}': chr(0x00e8),# 7/D e grave 286 | '~': chr(0x00e0)# 7/E a grave 287 | } 288 | return mapper.get(c, c) 289 | 290 | def mapIT(c): # Italian Nat. Opt. 6, group 0 291 | mapper = { 292 | '#': chr(0x00a3),# 2/3 Pound 293 | # '$': '$' # 2/4 Dollar sign not mapped 294 | '@': chr(0x00e9),# 4/0 e acute 295 | '[': chr(0x00b0),# 5/B ring 296 | '\\': chr(0x00e7),# 5/C c cedilla 297 | # Nat. opt. 2 298 | ']': chr(0x2192),# 5/D right arrow 299 | '^': chr(0x2191),# 5/E up arrow 300 | '_': '#', # 5/F hash 301 | '`': chr(0x00f9),# 6/0 u grave 302 | '{': chr(0x00e0),# 7/B a grave 303 | '|': chr(0x00f2),# 7/C o grave 304 | '}': chr(0x00e8),# 7/D e grave 305 | '~': chr(0x00ec)# 7/E i grave 306 | } 307 | return mapper.get(c, c) 308 | 309 | def mapPL(c): # Polish Nat. Opt. 6, region 1# 310 | mapper = { 311 | #'#': chr(0x0023), # 2/3 # is not mapped 312 | '$': chr(0x0144), # 2/4 - n acute 313 | '@': chr(0x0105), # 4/0 - a ogonek 314 | '[': chr(0x01b5), # 5/B - z stroke 315 | "\\": chr(0x015a), # 5/C - S acute 316 | ']': chr(0x0141), # 5/D - L stroke 317 | '^': chr(0x0107), # 5/E - c acute 318 | '_': chr(0x00f3), # 5/F - o acute 319 | '`': chr(0x0119), # 6/0 - e ogonek 320 | '{': chr(0x017c), # 7/B - s overdot 321 | '|': chr(0x015b), # 7/C - s acute 322 | '}': chr(0x0142), # 7/D - I stroke 323 | '~': chr(0x017a) # 7/E - z acute 324 | } 325 | return mapper.get(c, c) 326 | 327 | def mapTR(c): # Turkish Nat. Opt. 2, region 3 328 | mapper = { 329 | '#': chr(0x0167), # 2/3 330 | '$': chr(0x011f), # 2/4 331 | '@': chr(0x0130), # 4/0 332 | '[': chr(0x015e), # 5/B 333 | '\\': chr(0x00d6), # 5/C 334 | ']': chr(0x00c7), # 5/D 335 | '^': chr(0x00dc), # 5/E 336 | '_': chr(0x011e), # 5/F 337 | '`': chr(0x0131), # 6/0 338 | '{': chr(0x015f), # 7/B 339 | '|': chr(0x00f6), # 7/C 340 | '}': chr(0x00e7), # 7/D 341 | '~': chr(0x00fc) # 7/E 342 | } 343 | return mapper.get(c, c) 344 | 345 | def mapRS(c): # Latin G0 Set - Option 2 Serbian/Croatian/Slovenian Nat. Opt. 2, region 3 346 | mapper = { 347 | '#': chr(0x0023), # 2/3 348 | '$': chr(0x00cb), # 2/4 349 | '@': chr(0x010c), # 4/0 350 | '[': chr(0x0106), # 5/B 351 | '\\': chr(0x017d), # 5/C 352 | ']': chr(0x0110), # 5/D 353 | '^': chr(0x0160), # 5/E 354 | '_': chr(0x00eb), # 5/F 355 | '`': chr(0x010d), # 6/0 356 | '{': chr(0x0107), # 7/B 357 | '|': chr(0x017e), # 7/C 358 | '}': chr(0x0111), # 7/D 359 | '~': chr(0x0161) # 7/E 360 | } 361 | return mapper.get(c, c) 362 | 363 | def mapRO(c): # Latin G0 Set - Option 7 Romanian Nat. Opt. 2, region 3 364 | mapper = { 365 | '#': chr(0x0023), # 2/3 366 | '$': chr(0x00a4), # 2/4 367 | '@': chr(0x0162), # 4/0 368 | '[': chr(0x00c2), # 5/B 369 | '\\': chr(0x015e), # 5/C 370 | ']': chr(0x0102), # 5/D 371 | '^': chr(0x00ce), # 5/E 372 | '_': chr(0x0131), # 5/F 373 | '`': chr(0x0163), # 6/0 374 | '{': chr(0x00e2), # 7/B 375 | '|': chr(0x015f), # 7/C 376 | '}': chr(0x0103), # 7/D 377 | '~': chr(0x00ee) # 7/E 378 | } 379 | return mapper.get(c, c) 380 | 381 | # region 4 382 | def mapRU(c): # Cyrillic G0 Set - Option 2 Russian/Bulgarian, region 4 383 | mapper = { 384 | # Not sure this is all correct 385 | # Nat. opt. 2. Column 40-4f 386 | '@': chr(0x042e), # Cyrillic Capital Letter Yu 387 | 'C': chr(0x0426), # Cyrillic 388 | 'D': chr(0x0414), # 389 | 'E': chr(0x0415), 390 | 'F': chr(0x0424), 391 | 'G': chr(0x0413), # 392 | 'H': chr(0x0425), # 393 | # Cyrillic G0 Column 50-5f 394 | 'Q': chr(0x042f), 395 | 'R': chr(0x0420), 396 | 'S': chr(0x0421), 397 | 'T': chr(0x0422), 398 | 'U': chr(0x0423), 399 | 'V': chr(0x0416), 400 | 'W': chr(0x0412), 401 | 'X': chr(0x042c), 402 | 'Y': chr(0x042a), 403 | 'Z': chr(0x0417), 404 | '[': chr(0x0428), # Nap opt 2 starts here 405 | '\\': chr(0x042d), 406 | ']': chr(0x0429), 407 | '^': chr(0x0427), 408 | '_': chr(0x042b), 409 | # Cyrillic G0 Column 60-6f 410 | '`': chr(0x044e), # Nat opt 2 stops here 411 | # 'a': chr(0x0430), 412 | # 'b': chr(0x0431), 413 | 'c': chr(0x0446), 414 | 'd': chr(0x0434), 415 | 'e': chr(0x0435), 416 | 'f': chr(0x0444), 417 | 'g': chr(0x0433), 418 | 'h': chr(0x0445), 419 | 'i': chr(0x0438), 420 | 'j': chr(0x0439), 421 | # Remaining are OK 422 | # Cyrillic G0 Column 70-7f 423 | # 70 is OK 424 | 'q': chr(0x044f), 425 | 'r': chr(0x0440), 426 | 's': chr(0x0441), 427 | 't': chr(0x0442), 428 | 'u': chr(0x0443), 429 | 'v': chr(0x0436), 430 | 'w': chr(0x0432), 431 | 'x': chr(0x044c), 432 | 'y': chr(0x044a), 433 | 'z': chr(0x0437), 434 | '{': chr(0x0448), 435 | '|': chr(0x044d), 436 | '}': chr(0x0449), 437 | '~': chr(0x0447) 438 | # Remaining are OK 439 | } 440 | return mapper.get(c, c) 441 | 442 | 443 | def mapEE(c): # Latin G0 Set - Option 2 Estonian, region 4 444 | return mapCZ(c) 445 | 446 | 447 | def mapUA(c): # Ukrainian (Cyrillic), region 4 448 | mapper = { 449 | # Nat. opt. 2. Column 40-4f 450 | '@': chr(0x042e), # Cyrillic Capital Letter Yu 451 | 'C': chr(0x0426), # Cyrillic 452 | 'D': chr(0x0414), # 453 | 'E': chr(0x0415), 454 | 'F': chr(0x0424), 455 | 'G': chr(0x0413), # 456 | 'H': chr(0x0425), # 457 | # Cyrillic G0 Column 50-5f 458 | 'Q': chr(0x042f), # 5/1 459 | 'R': chr(0x0420), # 5/2 460 | 'S': chr(0x0421), 461 | 'T': chr(0x0422), 462 | 'U': chr(0x0423), 463 | 'V': chr(0x0416), 464 | 'W': chr(0x0412), 465 | 'X': chr(0x042c), 466 | 'Y': chr(0x0406), # 5/8 042a russian 467 | 'Z': chr(0x0417), # 5/9 468 | '[': chr(0x0428), # Nat opt 2 starts here 469 | '\\': chr(0x0404), # 5/c Russian 042d 470 | ']': chr(0x0429), # 5/d 471 | '^': chr(0x0427), # 5/e 472 | '_': chr(0x0407), # 5/f russian 042b 473 | # Cyrillic G0 Column 60-6f 474 | '`': chr(0x044e), # 6/0 475 | # 'a': chr(0x0430), # 6/1 476 | # 'b': chr(0x0431), 477 | 'c': chr(0x0446), 478 | 'd': chr(0x0434), 479 | 'e': chr(0x0435), 480 | 'f': chr(0x0444), 481 | 'g': chr(0x0433), 482 | 'h': chr(0x0445), 483 | 'i': chr(0x0438), 484 | 'j': chr(0x0439), 485 | # Remaining are OK 486 | # Cyrillic G0 Column 70-7f 487 | 'p': chr(0x006e), # 7/0 Use lower case n for Ukrainian 488 | 'q': chr(0x044f), # 7/1 489 | 'r': chr(0x0440), # 7/2 490 | 's': chr(0x0441), # 7/3 491 | 't': chr(0x0442), # 7/4 492 | 'u': chr(0x0443), # 7/5 493 | 'v': chr(0x0436), # 7/6 494 | 'w': chr(0x0432), # 7/7 495 | 'x': chr(0x044c), # 7/8 496 | 'y': chr(0x0456), # 7/9 russian 044a 497 | 'z': chr(0x0437), # 7/a 498 | '{': chr(0x0448), # 7/b 499 | '|': chr(0x0454), # 7/c russian 044d 500 | '}': chr(0x0449), # 7/d 501 | '~': chr(0x0447) # 7/e russian 0447 502 | # Remaining are OK 503 | } 504 | return mapper.get(c, c) 505 | 506 | def mapLV(c): # Lettish/Lithuanian (Latin) region 4, option 6 507 | mapper = { 508 | '#': chr(0x0023), # 2/3 509 | '$': chr(0x0024), # 2/4 510 | '@': chr(0x0160), # 4/0 511 | '[': chr(0x0117), # 5/B 512 | '\\': chr(0x0229), # 5/C 513 | ']': chr(0x017d), # 5/D 514 | '^': chr(0x010d), # 5/E 515 | '_': chr(0x016b), # 5/F 516 | '`': chr(0x0161), # 6/0 517 | '{': chr(0x0105), # 7/B 518 | '|': chr(0x0173), # 7/C 519 | '}': chr(0x017e), # 7/D 520 | '~': chr(0x012f) # 7/E This is the best match in teletext2 521 | } 522 | return mapper.get(c, c) 523 | 524 | def mapGR(c): # Greek region 6, option 7 525 | if c=='R': 526 | return chr(0x0374); # Top right dot thingy 527 | if c>='@' and c<='~': 528 | return chr(ord(c)+0x390-ord('@')) 529 | if c=='<': 530 | return chr(0x00ab) # left chevron 531 | if c=='>': 532 | return chr(0x00bb) # right chevron 533 | return c 534 | 535 | def mapAR(c): # Arabic region 8, option 7 536 | if c=='>': 537 | return '<'; # 3/c 538 | if c=='<': 539 | return '>'; # 3/e 540 | return chr(ord(c)+0xe606-ord('&')) # 2/6 onwards 541 | 542 | def mapHE(c): # Hebrew region 10, option 5 543 | if (c>0x5f) and (c<0x7b): # Hebrew characters 544 | return chr(ord(c)+0x05d0-0x60) 545 | mapper = { 546 | '#': chr(0x00A3), # 2/3 # is mapped to pound sign 547 | '[': chr(0x2190), # 5/B Left arrow. 548 | '\\': chr(0xbd), # 5/C Half 549 | ']': chr(0x2192), # 5/D Right arrow. 550 | '^': chr(0x2191), # 5/E Up arrow. 551 | '_': chr(0x0023), # 5/F Underscore is hash sign 552 | '{': chr(0x20aa), # 7/B sheqel 553 | '|': chr(0x2016), # 7/C Double pipe 554 | '}': chr(0xbe), # 7/D Three quarters 555 | '~': chr(0x00f7) # 7/E Divide 556 | } 557 | return mapper.get(c, c) 558 | 559 | # @param ch - character to map 560 | # @param diacritical to add (if possible) 0..15 from row 0x40 of Latin G2 561 | # NOTE. This is now split. Characters are looked up during reading the page by getdiacritical 562 | # mapdiacritical is now only used to render the character 563 | def mapdiacritical(ch, row, col, diacritical): 564 | # print("[mapdiacritical] diacriticals on this page = " + str(len(diacritical))) 565 | for i in range(0, len(diacritical)): 566 | d = diacritical[i] 567 | if row==d[0] and col==d[1] : # match character location 568 | return chr(d[2]) # This is the correct character 569 | return ch # None found 570 | 571 | # Page 95: Characters Including Diacritical Marks 572 | # @param ch - Character to add a diacritical to 573 | # @param accent - accent to add to the character (from X26/0) 574 | # @return The accented character from the teletext font 575 | def getdiacritical(ch, accent): 576 | # print("[getdiacritical] get diacriticals 577 | if accent == 0: # grave 578 | if ch=='A': 579 | return chr(0xc0) 580 | if ch=='E': 581 | return chr(0xc8) 582 | if ch=='I': 583 | return chr(0xcc) 584 | if ch=='O': 585 | return chr(0xd2) 586 | if ch=='U': 587 | return chr(0xd9) 588 | if ch=='a': 589 | return chr(0xe0) 590 | if ch=='e': 591 | return chr(0xe8) 592 | if ch=='i': 593 | return chr(0xec) 594 | if ch=='o': 595 | return chr(0xf2) 596 | if ch=='u': 597 | return chr(0xf9) 598 | # Cyrillics go here 599 | if accent == 2: # acute 600 | if ch=='A': 601 | return chr(0xc1) 602 | if ch=='E': 603 | return chr(0xc9) 604 | if ch=='I': 605 | return chr(0xcd) 606 | if ch=='O': 607 | return chr(0xd3) 608 | if ch=='U': 609 | return chr(0xda) 610 | if ch=='Y': 611 | return chr(0xdd) 612 | if ch=='a': 613 | return chr(0xe1) 614 | if ch=='e': 615 | return chr(0xe9) 616 | if ch=='i': 617 | return chr(0xed) 618 | if ch=='o': 619 | return chr(0xf3) 620 | if ch=='u': 621 | return chr(0xfa) 622 | if ch=='y': 623 | return chr(0xfd) 624 | if ch=='C': 625 | return chr(0x106) 626 | if ch=='c': 627 | return chr(0x107) 628 | if ch=='c': 629 | return chr(0x139) 630 | if ch=='l': 631 | return chr(0x13a) 632 | if ch=='N': 633 | return chr(0x143) 634 | if ch=='n': 635 | return chr(0x144) 636 | if ch=='R': 637 | return chr(0x154) 638 | if ch=='r': 639 | return chr(0x155) 640 | if ch=='S': 641 | return chr(0x15a) 642 | if ch=='s': 643 | return chr(0x15b) 644 | if accent == 3: # circumflex 645 | if ch=='A': 646 | return chr(0xc2) 647 | if ch=='E': 648 | return chr(0xca) 649 | if ch=='I': 650 | return chr(0xce) 651 | if ch=='O': 652 | return chr(0xd4) 653 | if ch=='U': 654 | return chr(0xd8) 655 | if ch=='a': 656 | return chr(0xe2) 657 | if ch=='e': 658 | return chr(0xea) 659 | if ch=='i': 660 | return chr(0xee) 661 | if ch=='o': 662 | return chr(0xf4) 663 | if ch=='u': 664 | return chr(0xfb) 665 | if ch=='C': 666 | return chr(0x108) 667 | if ch=='c': 668 | return chr(0x109) 669 | if ch=='G': 670 | return chr(0x11c) 671 | if ch=='g': 672 | return chr(0x11d) 673 | if ch=='H': 674 | return chr(0x124) 675 | if ch=='h': 676 | return chr(0x125) 677 | if ch=='J': 678 | return chr(0x134) 679 | if ch=='j': 680 | return chr(0x135) 681 | if ch=='S': 682 | return chr(0x15c) 683 | if ch=='s': 684 | return chr(0x15d) 685 | if ch=='W': 686 | return chr(0x174) 687 | if ch=='w': 688 | return chr(0x175) 689 | if ch=='Y': 690 | return chr(0x176) 691 | if ch=='y': 692 | return chr(0x177) 693 | 694 | if accent == 4: # tilde ~ 695 | if ch=='A': 696 | return chr(0xc3) 697 | if ch=='N': 698 | return chr(0xd1) 699 | if ch=='O': 700 | return chr(0xd5) 701 | if ch=='a': 702 | return chr(0xe3) 703 | if ch=='n': 704 | return chr(0xf1) 705 | if ch=='o': 706 | return chr(0xf5) 707 | if ch=='I': 708 | return chr(0x128) 709 | if ch=='i': 710 | return chr(0x129) 711 | if ch=='U': 712 | return chr(0x168) 713 | if ch=='u': 714 | return chr(0x169) 715 | 716 | if accent == 5: # macron (over line) 717 | if ch=='A': 718 | return chr(0x100) 719 | if ch=='a': 720 | return chr(0x101) 721 | if ch=='E': 722 | return chr(0x112) 723 | if ch=='e': 724 | return chr(0x113) 725 | if ch=='I': 726 | return chr(0x12a) 727 | if ch=='i': 728 | return chr(0x12b) 729 | if ch=='O': 730 | return chr(0x14c) 731 | if ch=='o': 732 | return chr(0x14d) 733 | if ch=='U': 734 | return chr(0x16a) 735 | if ch=='u': 736 | return chr(0x16b) 737 | 738 | if accent == 6: # breve 739 | if ch=='A': 740 | return chr(0x102) 741 | if ch=='a': 742 | return chr(0x103) 743 | if ch=='E': 744 | return chr(0x114) 745 | if ch=='e': 746 | return chr(0x115) 747 | if ch=='G': 748 | return chr(0x11e) 749 | if ch=='g': 750 | return chr(0x11f) 751 | if ch=='I': 752 | return chr(0x12c) 753 | if ch=='i': 754 | return chr(0x12d) 755 | if ch=='O': 756 | return chr(0x14e) 757 | if ch=='o': 758 | return chr(0x14f) 759 | 760 | if accent == 7: # dot above 761 | if ch=='C': 762 | return chr(0x10A) 763 | if ch=='c': 764 | return chr(0x10B) 765 | if ch=='E': 766 | return chr(0x116) 767 | if ch=='e': 768 | return chr(0x117) 769 | if ch=='G': 770 | return chr(0x120) 771 | if ch=='g': 772 | return chr(0x121) 773 | if ch=='I': 774 | return chr(0x130) 775 | if ch=='Z': 776 | return chr(0x17B) 777 | if ch=='z': 778 | return chr(0x17C) 779 | 780 | if accent == 8: # diaresis above 781 | if ch=='A': 782 | return chr(0xc4) 783 | if ch=='E': 784 | return chr(0xcb) 785 | if ch=='I': 786 | return chr(0xcf) 787 | if ch=='O': 788 | return chr(0xd6) 789 | if ch=='U': 790 | return chr(0xdc) 791 | if ch=='a': 792 | return chr(0xe4) 793 | if ch=='e': 794 | return chr(0xeb) 795 | if ch=='i': 796 | return chr(0xef) 797 | if ch=='o': 798 | return chr(0xf6) 799 | if ch=='u': 800 | return chr(0xfc) 801 | if ch=='y': 802 | return chr(0xff) 803 | 804 | #if accent == 9: # low acute 805 | 806 | if accent == 10: # Ring above (0x4a) 807 | if ch=='A': 808 | return chr(0xc5) 809 | if ch=='a': 810 | return chr(0xe5) 811 | if ch=='U': 812 | return chr(0x16e) 813 | if ch=='u': 814 | return chr(0x16f) 815 | 816 | if accent == 11: # Cedilla (0x4b) 817 | if ch=='C': 818 | return chr(0xc7) 819 | if ch=='c': 820 | return chr(0xe7) 821 | if ch=='G': 822 | return chr(0x122) 823 | if ch=='g': 824 | return chr(0x123) 825 | if ch=='K': 826 | return chr(0x136) 827 | if ch=='k': 828 | return chr(0x137) 829 | if ch=='L': 830 | return chr(0x13b) 831 | if ch=='l': 832 | return chr(0x13c) 833 | if ch=='N': 834 | return chr(0x145) 835 | if ch=='n': 836 | return chr(0x146) 837 | if ch=='R': 838 | return chr(0x156) 839 | if ch=='r': 840 | return chr(0x157) 841 | if ch=='S': 842 | return chr(0x15e) 843 | if ch=='s': 844 | return chr(0x15f) 845 | if ch=='T': 846 | return chr(0x162) 847 | if ch=='t': 848 | return chr(0x163) 849 | if ch=='e': 850 | return chr(0x229) 851 | 852 | # if accent == 12: # low macron 853 | 854 | if accent == 13: # double acute 0x4d 855 | if ch=='O': 856 | return chr(0x150) 857 | if ch=='o': 858 | return chr(0x104) 859 | if ch=='U': 860 | return chr(0x170) 861 | if ch=='u': 862 | return chr(0x171) 863 | 864 | if accent == 14: # ogonek 0x4e 865 | if ch=='A': 866 | return chr(0x104) 867 | if ch=='a': 868 | return chr(0x105) 869 | if ch=='E': 870 | return chr(0x118) 871 | if ch=='e': 872 | return chr(0x119) 873 | if ch=='I': 874 | return chr(0x12e) 875 | if ch=='i': 876 | return chr(0x12f) 877 | if ch=='U': 878 | return chr(0x172) 879 | if ch=='u': 880 | return chr(0x173) 881 | 882 | if accent == 15: # caron 0x4f 883 | if ch=='C': 884 | return chr(0x10c) 885 | if ch=='c': 886 | return chr(0x10d) 887 | if ch=='D': 888 | return chr(0x10e) 889 | if ch=='d': 890 | return chr(0x10f) 891 | if ch=='E': 892 | return chr(0x11a) 893 | if ch=='e': 894 | return chr(0x11b) 895 | if ch=='L': 896 | return chr(0x13d) 897 | if ch=='l': 898 | return chr(0x13e) 899 | if ch=='N': 900 | return chr(0x147) 901 | if ch=='n': 902 | return chr(0x148) 903 | if ch=='R': 904 | return chr(0x158) 905 | if ch=='r': 906 | return chr(0x159) 907 | if ch=='S': 908 | return chr(0x160) 909 | if ch=='s': 910 | return chr(0x161) 911 | if ch=='T': 912 | return chr(0x164) 913 | if ch=='t': 914 | return chr(0x165) 915 | if ch=='Z': 916 | return chr(0x17d) 917 | if ch=='z': 918 | return chr(0x17e) 919 | if ch=='A': 920 | return chr(0x1cd) 921 | if ch=='a': 922 | return chr(0x1ce) 923 | return ch # Not possible to accent, it was an X26 substituted character 924 | 925 | # @param ch - character to convert to G2 (ordinal value) 926 | # @return G2 mapping of the character number 927 | def MapLatinG2(ch): # Page 116 Latin G2 Supplementary Set 928 | if ch <= 0x20 : 929 | return 0x20 930 | if ch==0x21: # inverted exclamation mark 931 | return 0xa1 932 | if ch==0x22: # cent 933 | return 0xa2 934 | if ch==0x23: # pound 935 | return 0xa3 936 | if ch==0x24: # dollar 937 | return 0x24 938 | if ch==0x25: # yen 939 | return 0xa5 940 | if ch==0x26: # hash 941 | return 0x23 942 | if ch==0x27: # section 943 | return 0xa7 944 | if ch==0x28: # currency 945 | return 0xa4 946 | if ch==0x29: # left single quote 947 | return 0x2018 948 | if ch==0x2a: # left double quote 949 | return 0x201c 950 | if ch==0x2b: # left pointing double angle quotation mark 951 | return 0xab 952 | if ch==0x2c: # left pointing arrow 953 | return 0x2190 954 | if ch==0x2d: # upwards arrow 955 | return 0x2191 956 | if ch==0x2e: # rightwards arrow 957 | return 0x2192 958 | if ch==0x2f: # downwards arrow 959 | return 0x2193 960 | # 30 961 | if ch==0x30: # degree 962 | return 0xb0 963 | if ch==0x31: # plus/minus 964 | return 0xb1 965 | if ch==0x32: # superscript 2 966 | return 0xb2 967 | if ch==0x33: # superscript 3 968 | return 0xb3 969 | if ch==0x78: # multiplication x 970 | return 0xd7 971 | if ch==0x35: # micro u 972 | return 0xb5 973 | if ch==0x36: # pilcrow 974 | return 0xb6 975 | if ch==0x37: # middle dot 976 | return 0xb7 977 | if ch==0x38: # division 978 | return 0xf7 979 | if ch==0x39: # right single quote 980 | return 0x2019 981 | if ch==0x3a: # right double quote 982 | return 0x201d 983 | if ch==0x3b: # right double angle quote 984 | return 0xbb 985 | if ch==0x3c: # quarter 986 | return 0xbc 987 | if ch==0x3d: # half 988 | return 0xbd 989 | if ch==0x3e: # three quarter 990 | return 0xbe 991 | if ch==0x3f: # inverted question mark 992 | return 0xbf 993 | 994 | # Diacriticals 995 | if ch==0x40: # space 996 | return 0x20 997 | if ch==0x41: # grave 998 | return 0x2cb 999 | if ch==0x42: # acute 1000 | return 0x2ca 1001 | if ch==0x43: # circumflex 1002 | return 0x2c6 1003 | if ch==0x44: # tilde 1004 | return 0x2dc 1005 | if ch==0x45: # macron 1006 | return 0x2c9 1007 | if ch==0x46: # breve 1008 | return 0x2d8 1009 | if ch==0x47: # dot above 1010 | return 0x2d9 1011 | if ch==0x48: # diaeresis 1012 | return 0xa8 1013 | if ch==0x49: # dot under (full stop :-o) 1014 | return 0x2e 1015 | if ch==0x4a: # ring above 1016 | return 0x2da 1017 | if ch==0x4b: # cedilla 1018 | return 0xb8 1019 | if ch==0x4c: # low macron 1020 | return 0x2cd 1021 | if ch==0x4d: # double acute 1022 | return 0xdd 1023 | if ch==0x4e: # ogonek 1024 | return 0x2db 1025 | if ch==0x4f: # caron 1026 | return 0x2c7 1027 | 1028 | # ... @todo 1029 | if ch==0x55: # quaver 1030 | return 0x266a 1031 | # up to 0x7f 1032 | print ("[MapLatinG2] @TODO. UNHANDLED MAPPING: " + str(ch)) 1033 | return ord('?') # 1034 | --------------------------------------------------------------------------------