Proportional Printing

This file is part of and SINCUS Exchange Tape 102 - Utilities & Business. Download the collection to get this file.
Developer(s): Wes Brzozowski
Date: 1985
Type: Program
Platform(s): TS 2068
Tags: Demo, Printer

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

LineObservation
1075Checks 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.
1200LET 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.
2025RANDOMIZE 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.

Content

Appears On

Library tape from the Sinclair Computer Users Society (SINCUS).

Related Products

Related Articles

Related Content

Image Gallery

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<smin THEN LET smin=s
 1100 NEXT x
 1110 LET addr=addr-8
 1120 FOR x=1 TO 8
 1130 POKE daddr,(PEEK addr)*smin
 1140 LET addr=addr+1: LET daddr=daddr+1
 1150 NEXT x
 1160 LET smin=8: LET daddr=daddr-8
 1170 FOR x=1 TO 8
 1180 LET s=0: LET r=PEEK daddr: LET daddr=daddr+1: IF r=0 THEN GO TO 1220
 1190 IF ABS (r/2-INT (r/2))>.1 THEN GO TO 1210
 1200 LET s=s+1: LET r=INT (r/2+.1): GO TO 1190
 1210 IF s<smin THEN LET smin=s
 1220 NEXT x
 1225 IF smin=0 THEN LET smin=1
 1226 LET q=2^(smin-1)-1: IF smin=1 THEN LET q=0
 1230 POKE daddr-8,q
 1240 NEXT c
 1250 POKE 64200,15
 1260 FOR j=64201 TO 64207: POKE j,0: NEXT j
 2000 BEEP .25,1: BEEP .25,15: BEEP .25,1: BEEP .25,15
 2010 SAVE CHR$ 253+CHR$ 245+CHR$ 8+"ING"+CHR$ 235+"YOU" LINE 3000
 2020 SAVE CHR$ 232+CHR$ 204+CHR$ 227+"THE "+CHR$ 175 CODE 64200,1160
 2025 RANDOMIZE USR 64970
 2030 GO SUB 8000
 2040 STOP 
 3000 CLEAR 64199: LOAD ""CODE 
 3010 RANDOMIZE USR 64970
 3020 GO SUB 8000
 3030 LPRINT 
 3040 LPRINT "You can now NEW the BASIC portion away;"
 3050 LPRINT "This will Turn Off the proportional printing..."
 3060 LPRINT "But you can turn it on again, with"
 3070 LPRINT TAB 60;"RANDOMIZE USR 64970"
 3080 STOP 
 8000 CLS : PRINT "This is an example of the boringold printing. What else could wewant?"
 8010 LPRINT AT 0,50;"Well, we COULD wish for proportional printing;"
 8020 LPRINT "Look how neat it is, and how easy it is to read!"
 8030 LPRINT "...Then, count how many additional characters   we can get on a line."
 8040 LPRINT 
 8050 LPRINT "REMEMBER...these characters are the SAME      SIZE as the standard Timex character set. Onlythe spacing between them has been changed!!!"
 8060 RETURN 

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.

Scroll to Top