--- title: "Proportional Printing" id: 53858 type: "computer_media" slug: "wesppp" url: "http://localhost/computer_media/wesppp/" markdown_url: "http://localhost/computer_media/wesppp.md" published_at: "2024-05-13T19:02:05+00:00" modified_at: "2026-03-30T21:44:50+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2019/02/20230809-041332.jpg" excerpt: "A self-building proportional text engine that compresses ROM fonts on the fly and patches the LPRINT channel to render neatly spaced characters on screen." category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" post_tag: - name: "Downloadable" slug: "downloadable" taxonomy: "post_tag" url: "http://localhost/tag/downloadable/" - name: "TS 2068" slug: "ts2068" taxonomy: "post_tag" url: "http://localhost/tag/ts2068/" model: - name: "Timex/Sinclair 2068" slug: "ts-2068" taxonomy: "model" url: "http://localhost/model/ts-2068/" indiv: - name: "Wes Brzozowski" slug: "wes-brzozowski" taxonomy: "indiv" url: "http://localhost/indiv/wes-brzozowski/" genre: - name: "Demo" slug: "demo" taxonomy: "genre" url: "http://localhost/type/demo/" - name: "Printer" slug: "printer" taxonomy: "genre" url: "http://localhost/type/printer/" media_contents: - id: 55361 title: "SINCUS Exchange Tape 102 – Utilities & Business" type: "computer_media" url: "http://localhost/computer_media/sincus-exchange-tape-102-utilities-business/" media_type: "Program" programmers: - name: "Wes Brzozowski" slug: "wes-brzozowski" taxonomy: "indiv" url: "http://localhost/indiv/wes-brzozowski/" mediadate: "1985" images: - url: "http://localhost/wp-content/uploads/2024/05/SCR-20240622-slgk.png" - url: "http://localhost/wp-content/uploads/2024/05/SCR-20240622-smsf.png" media_type_tags: "Demo, Printer" --- # Proportional Printing This program generates a proportional printing utility for the TS2068 by computing compressed font data from the system’s built-in character ROM and embedding a hand-crafted Z80 machine code routine. The machine code (387 bytes, POKEd into addresses 64970–65356) hooks the LPRINT mechanism so that all LPRINT statements render text to the screen with proportional spacing, while standard PRINT remains unaffected. Font compression is performed in BASIC (lines 1000–1240): for each of the 95 printable characters the routine scans all eight pixel rows to find the maximum horizontal shrink factor, multiplies each row by that factor, and stores a one-byte width hint in the compressed font table at address 64200. AT and TAB arguments are reinterpreted as pixel coordinates rather than character cell positions. After building everything in RAM, the loader SAVEs both the BASIC program and the code block to tape, then demonstrates proportional output live using LPRINT. *** ## Program Analysis ### Program Structure The listing divides into five logical phases: 1. **Remarks and setup (lines 10–60):** Documentation REMs explain purpose and usage; `GO TO 585` skips the subroutines and DATA blocks. 2. **Hex decoder subroutine and DATA (lines 70–580):** A subroutine at line 70 converts two-character ASCII hex strings into byte values; 480 DATA items encode the Z80 machine code payload. 3. **Loader entry and machine code installation (lines 585–700):** Clears memory to 64199, then POKEs 387 bytes into addresses 64970–65356 by calling the hex decoder in a loop. 4. **Font compression engine (lines 1000–1260):** Reads the system character ROM, derives a proportional width for each of 95 characters, and stores compressed bitmaps plus width hints at address 64200. 5. **Save, demo, and runtime program (lines 2000–8060):** SAVEs both a BASIC loader and the code/font block to tape, activates the routine with `RANDOMIZE USR 64970`, and demonstrates proportional output. ### Hex Decoder Subroutine (lines 70–95) The subroutine at line 70 reads a two-character string from DATA, extracts the high and low nibbles via `CODE`, and applies the standard ASCII-to-hex adjustment: if a character code exceeds 57 (`'9'`), 7 is subtracted to bridge the gap between `'9'` and `'A'`. The final expression `16*hi+lo-816` subtracts 816 (= 16×48 + 48) to remove the ASCII bias of both nibbles simultaneously, yielding a raw byte value 0–255. ### Machine Code Payload The Z80 routine occupies addresses 64970–65356 (387 bytes). Entry point `USR 64970` is called to activate the hook. Based on the DATA and the REMs, the routine patches the TS2068 LPRINT output channel so that characters are rendered to the display file using proportional spacing. Several system variables in the `FE` page (e.g. `FEE9h`, `FEEAh`, `FEEBh`, `FEEDh`) are used as working state for the current pixel column, row, and mode flags. The routine handles special codes including `AT` (22), `TAB` (23), carriage return (13), and a newline sequence, consistent with the TS2068 channel protocol. ### Font Compression Algorithm (lines 1000–1240) The algorithm operates on each of the 95 printable characters sourced from the ROM via the system variable at addresses 23606–23607 (the `CHARS` pointer). For each character it performs two passes: - **First pass (lines 1020–1110):** For each of the 8 pixel rows, the code finds the largest power-of-two divisor of the pixel byte (by checking thresholds 16, 32, 64, 128), tracking the minimum across all rows. This minimum (`smin`) is the horizontal compression factor. - **Scaling (lines 1120–1150):** Each row byte is multiplied by `smin` and stored at the destination address `daddr` (starting at 64208). - **Second pass (lines 1160–1230):** The scaled bytes are examined to find how many times they can all be right-shifted (halved) before any odd bit is encountered, yielding the character’s effective pixel width as a shift count stored as a one-byte prefix. Line 1225 guards against a zero `smin` (for blank characters like space), and line 1226 computes a bitmask `q = 2^(smin-1)-1` stored at the character’s width-hint position. Line 1250 then POKEs 15 into address 64200 (the space character’s width override), and lines 1251–1260 zero out the subsequent 7 bytes. ### Save Mechanism (lines 2010–2020) Line 2010 constructs the BASIC program filename using raw `CHR$` values rather than a plain string literal. The `LINE 3000` clause causes the saved BASIC program to auto-run from line 3000, which performs a `CLEAR`, `LOAD ""CODE`, and re-installs the hook. Line 2020 saves the combined font table and machine code as a CODE block starting at address 64200, length 1160 bytes, under a similarly constructed filename. ### Runtime Demonstration (lines 3000–8060) When the saved program is loaded and run, lines 3040–3070 use `LPRINT` (now hooked) to display proportional text, including a `TAB 60` at pixel column 60. The demo subroutine at line 8000 contrasts standard `PRINT` (boring, unspaced) with `LPRINT` (proportional). `LPRINT` with no argument (line 3030) likely triggers a newline in the proportional renderer. ### Key BASIC Idioms and Techniques - `PEEK 23606+256*PEEK 23607` — standard idiom to read a two-byte little-endian system variable (`CHARS`) from the system variable area. - `ABS (r/2-INT (r/2))>.1` (line 1190) — tests whether a number is odd without using `MOD`, which is unavailable as a direct operator in this BASIC dialect. - `RANDOMIZE USR 64970` — discards the return value of the machine code call cleanly, avoiding a crash if the routine returns a non-zero value. - `CLEAR 64199` — sets RAMTOP just below the machine code area, protecting it from BASIC’s memory allocator. - All filenames in `SAVE` statements are built with `CHR$` concatenation, encoding non-printable or special characters directly. ### Notable Anomalies | Line | Observation | | --- | --- | | `1075` | Checks `l<128` for a shift of 2, but the preceding check at line 1070 only covers `l<64`, leaving values 64–127 handled here. The progression (16, 32, 64, 128) correctly corresponds to shifts of 4, 3, 2, and 1 pixel columns. | | `1200` | `LET r=INT (r/2+.1)` uses a small epsilon (+0.1) to compensate for floating-point rounding when halving integer pixel bytes — a pragmatic workaround for the BASIC interpreter’s float arithmetic. | | `2025` | `RANDOMIZE USR 64970` appears between the two SAVE calls and the demo subroutine, activating the hook immediately in the loader program so the live demo (line 2030 → 8000) also uses proportional printing. | ## Source Code ``` 10 REM Program to perform Proportional Printing.WES BRZOZOWSKI 15 REM An upgraded version of an entry in "YOUR SPECTRUM", Nov 1985 20 REM Changes include - Modified for TS2068, Supports TAB, better fonts, and works as in OVER 0, instead of OVER 1 25 REM When you RUN this program, it will SAVE the actual Proportional Print program to tape 30 REM When THAT program is run, all LPRINT statements will do proportional printing to the screen. 40 REM AT and TAB are supported, but they now refer to pixel positions, instead of character positions. 50 REM It will also be possible to use PRINT, to do non-proportional printing on the screen. 60 GO TO 585 70 REM Subroutine to decode the following Hexadecimal DATA statements 75 READ n$: LET hi=CODE n$(1): LET lo=CODE n$(2) 80 IF hi>57 THEN LET hi=hi-7 85 IF lo>57 THEN LET lo=lo-7 90 LET n=16*hi+lo-816 95 RETURN 100 DATA "21","00","A8","22","E9","FE","2A","4F" 110 DATA "5C","01","0F","00","09","01","DE","FD" 120 DATA "71","23","70","C9","E5","C5","D5","F5" 130 DATA "CD","EA","FD","F1","D1","C1","E1","C9" 140 DATA "F5","3A","F0","FE","FE","00","20","15" 150 DATA "F1","FE","16","20","06","3E","FF","32" 160 DATA "F0","FE","C9","FE","17","20","38","3E" 170 DATA "FD","32","F0","FE","C9","FE","FF","20" 180 DATA "09","F1","32","E9","FE","21","F0","FE" 190 DATA "35","C9","FE","FE","20","0D","F1","47" 200 DATA "3E","A8","90","32","EA","FE","AF","32" 210 DATA "F0","FE","C9","FE","FD","20","0A","F1" 220 DATA "32","E9","FE","3E","FC","32","F0","FE" 230 DATA "C9","F1","AF","32","F0","FE","C9","FE" 240 DATA "0D","20","09","CD","2A","FF","3E","02" 250 DATA "CD","30","12","C9","FE","20","38","04" 260 DATA "FE","80","38","02","3E","3F","26","00" 270 DATA "6F","29","29","29","EB","2A","F4","FE" 280 DATA "19","7E","32","F1","FE","36","00","22" 290 DATA "F2","FE","01","07","00","09","22","EE" 300 DATA "FE","3A","EA","FE","FE","A9","D2","0A" 310 DATA "FF","CD","13","FF","ED","4B","E9","FE" 320 DATA "CD","03","26","32","ED","FE","22","EB" 330 DATA "FE","06","08","C5","2A","EE","FE","7E" 340 DATA "2B","22","EE","FE","6F","3A","F1","FE" 350 DATA "16","FF","5F","4F","3A","ED","FE","FE" 360 DATA "00","28","12","47","26","00","CB","3D" 370 DATA "CB","1C","CB","3B","CB","FB","CB","1A" 380 DATA "A7","10","F3","42","4B","ED","5B","EB" 390 DATA "FE","1A","A1","B5","12","CD","37","FF" 400 DATA "3A","ED","FE","FE","00","28","09","13" 410 DATA "1A","A0","B4","12","CD","37","FF","1B" 420 DATA "2A","EB","FE","CD","F7","FE","22","EB" 430 DATA "FE","C1","10","AF","3A","F1","FE","2A" 440 DATA "F2","FE","77","3A","E9","FE","47","3A" 450 DATA "F6","FE","80","32","E9","FE","C9","00" 460 DATA "A8","00","00","00","00","00","00","00" 470 DATA "00","00","C8","F9","00","F5","7C","25" 480 DATA "E6","07","20","0A","7D","D6","20","6F" 490 DATA "38","04","7C","C6","08","67","F1","C9" 500 DATA "3A","F1","FE","2A","F2","FE","77","CF" 510 DATA "04","06","08","3A","F1","FE","4F","A7" 520 DATA "CB","39","30","03","05","18","F8","78" 530 DATA "32","F6","FE","3A","E9","FE","80","D0" 540 DATA "AF","32","E9","FE","3A","EA","FE","D6" 550 DATA "08","32","EA","FE","C9","E5","F5","7A" 560 DATA "CB","0F","CB","0F","CB","0F","E6","03" 570 DATA "F6","58","67","6B","3A","8D","5C","77" 580 DATA "F1","E1","C9" 585 CLEAR 64199: PRINT AT 10,0;"This Will Take a While...": PRINT " ...Why not take a break?" 590 REM *********************** 610 FOR j=64970 TO 65356: GO SUB 70: POKE j,n: NEXT j 700 REM *********************** 710 REM Now that the machine code is in, we'll derive the compressed fonts from the standard Timex character set 1000 LET addr=PEEK 23606+256*PEEK 23607+256+8: LET daddr=64208 1010 FOR c=1 TO 95 1020 LET smin=16 1030 FOR x=1 TO 8 1040 LET l=PEEK addr: LET addr=addr+1 1050 IF l<16 THEN LET s=16: GO TO 1090 1060 IF l<32 THEN LET s=8: GO TO 1090 1070 IF l<64 THEN LET s=4: GO TO 1090 1075 IF l<128 THEN LET s=2: GO TO 1090 1080 LET s=1 1090 IF s.1 THEN GO TO 1210 1200 LET s=s+1: LET r=INT (r/2+.1): GO TO 1190 1210 IF s