The Infamous GET & PUT (Graphics)

This is an in-depth discussion on how to use GET & PUT (Graphics) and how QB handles them.

 1. GET & PUT ARRAY FORMAT

 Overview of Image Format Byte Description 1-2 Width * 8 3-4 Height 5-END Image Data

I generally use INTEGER arrays. Therefore, this explination will be based on integer arrays.

1 byte of data has a range from 0-255, the same as 1 ASCII character. Therefore, a string with 8 characters is 8 bytes long.

In a 256 color environment (SCREEN 13), each color has a range of 0-255, the same as a 1 byte character. Thus, in order to define a color value, 1 byte of data is required.

Because a short integer (range = -32768 - 32767) uses two bytes per element (i.e. array(0) is two bytes worth of data), each element contains two bytes and thus can hold two colors. This is why an array holding data for a full screen shot (320 X 200) needs not be dimensioned to 64000 (320 * 200) elements, but only 32002 elements (64000/2 = 32000 & two elements are required for size info)

Thus, the general equation for dimensioning INTEGER arrays is w% * h% \ 2 + 3
I say plus 3 because if w% * h% produces an odd number, say 5 * 7 = 35, only 35 / 2 = 17.5 elements are required. Because you cannont have half of an element, 18 is used. If the dimensions are even, you simply are left with two extra bytes. No problem.

Let's examine the following example:
SCREEN 13
DIM ARRAY(0 TO 5 * 5 \ 2 + 3) AS INTEGER
GET (1, 1)-(5, 5), ARRAY

At this point, QBASIC has stored image data from the screen into ARRAY.
You can access all of this information.
ARRAY(0) should be equal to 40. This is because the first two bytes of the image array makeup the Width of the image times 8 (don't ask me why...) 5 * 8 = 40, therefore ARRAY(0) = 40.
ARRAY(1) should be equal to 5, the height of the image. It is not multiplyed by anything.
Be sure to use integer arrays to make things simple. If you do not specify, QBASIC defaults to single prescision arrays which use four bytes per element. Thus, all the size info is stored in the first element, and is more difficult to read.

We've already discussed how to access the Width and Height, now for the image data.

The data is stored byte for byte beginning with the upper left hand corner of the image. The data goes left to right, up to down. Thus, if you want the pixel in the upper left hand corner, it is byte 1, the byte immediately to the right is byte 2. The byte right below it is ImageWidth + 1.

In our 5 X 5 image example above, the data is arranged like so:
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25

Accessing this data can be a bit tricky. Because each array element in an integer array is two bytes, element 6 consists of bytes 13 and 14 (Note element six is not the sixth element. It is the seventh because the first element is zero). When put together, these two bytes may make any value between -32768 and 32767. So to read them, a single byte must be extracted. This is accomplished with PEEK:

DEF SEG = VARSEG(ARRAY(0)) '// Sets the segment address for reading from the array
c% = PEEK(13)
DEF SEG

This function retrieves the 14th byte from the integer array ARRAY. (You can retrieve byte 0.)

Recall that the first four bytes consist of image size information. Thus, the first pixel is located at offset 4.
To retrieve a pixel at location (X%, Y%) use the following formula:
COLOUR% = PEEK(1& * Y% * W% + X% + 4)

I added a '1&' so that QBASIC treats the operation as a long integer. This is because the maximum short integer value is 32767 and if the width was 320 and the height was 200, Y% * W% would be 64000. If the 1& were not included, QBASIC would return an error: "OVERFLOW"

PEEK is used to retrieve bytes, but POKE can be used in exactly the same manner to set bytes. Let's say you want the pixel at location (3, 4) of a 5 X 7 image array to be changed to a color value of 123.

W% = ARRAY(0) \ 8 '// SHOULD BE 5
H% = ARRAY(1) '// SHOULD BE 7
X% = 3
Y% = 4

DEF SEG = VARSEG(ARRAY(0))
POKE Y% * W% + X% + 4, 123 '// POKE BYTE 123 INTO LOCATION Y% * W% + X% + 4
DEF SEG

Be careful using POKE. It is quite possible to poke an area of memory not set aside for an array. It may cause the program to completely exit, if in Windows or freeze up the machine.
If you omit the DEF SEG statement or poke beyond the dimensions of the array, you may face dire consequences.

 2. TIPS & HITS FOR USAGE

There are many applications for PEEK and POKE. The first of which involves a PCOPY like use for screen 13.

You may have heard that POKE is faster than PSET. It is possible to set the segment address (DEF SEG = ) to the screen address (&HA000). Then you can POKE data directly into the video memory.

DEF SEG = &HA000
POKE Y% * 320& + X%
DEF SEG

Notice the simblance to the explination for array POKEing. You just omit the + 4 as this is not an array.
Well, what is we DID use an array? It would be almost identical, except you must add the + 4 to the POKEing address. Then, you could draw all of your graphics and in one command PUT them onto the screen.
Observe:

SUB PPSET(X%, Y%, COLOUR%)
POKE Y% * 320& + X% + 4, COLOUR%
END SUB

SCREEN 13

DO
REDIM SCREENBUFFER(32001) AS INTEGER
SCREENBUFFER(0) = 320 * 8
SCREENBUFFER(1) = 200
DEF SEG = VARSEG(SCREENBUFFER(0))

FOR I% = 1 TO 100
PPSET INT(RND * 320), INT(RND * 200), INT(RND * 256)
NEXT I%

PUT(0, 0), SCREENBUFFER, PSET
LOOP INKEY\$ <> ""

ERASE SCREENBUFFER
DEF SEG
END

This example draws 100 random pixels in random colors onto the screen, except that it draws them inside the array (which is not shown on the screen) and then all at once "PUTed" onto the screen. This operates somewhat like PCOPY. Note that I only used DEF SEG = VARSEG(SCREENBUFFER(0)) once in the loop. If it is not set again, this does not change so it does not need to be set before every POKE statement.

At the end of the program, DEF SEG is used to reset the segment address before ending the program. This is not really neccessary, but a good idea nonetheless.

The only drawbacks are that this only works for drawing pixels on the screen. However, you can make your own line and circle functions. They would be slower, of course, than QBASIC. The main use I've seen of this is for 3D shaded polygon examples which draw the figure pixel by pixel.

MULTIPLE SPRITES
Another application for sprite manipulation is useful for fonts. If the fonts are not drawn at runtime, that is, they are created and stored like sprites, then they can be quite nice looking, but difficult to recall. It is possible to store more than one image in an image array:

GET (X1, Y1)-(X2, Y2), ARRAY(OFFSET%)
PUT (X1, Y1), ARRAY(OFFSET%)

OFFSET% can be any integer value from 0 to 32767. If the OffSet% occurs at the end of another sprite, multiple spries can be stored and accessed from one array. If, like a font may be, all the images were the same size, then OFFSET% would be

SIZE% * (INDEX% - 1)

I say INDEX% - 1 because the first OFFSET% is zero. SIZE% * 0 = 0
SIZE% = 1& * X% * Y% \ 2 + 3

However, if the images are all the same size, another method could be used:

DIM ARRAY(NUMBEROFIMAGES%, SIZE%) AS INTEGER
FOR I% = 1 TO NUMBEROFIMAGES%
GET (X1, Y1)-(X2, Y2), ARRAY(I%, 0)
NEXT
PUT (X, Y), ARRAY(IMAGEWANTED%, 0), PSET

However, giving the image array two elements is only useful if the images are exactly the same size, such as fonts. Let's say you wanted to store images of different sizes.
The file Secret Weapon Software program contained in DIGGIF.ZIP (Digital Graphics Maker) available at http://jump.to/swsoft is an excellent example of this. Images are loaded from a file into QB memory and their OFFSETS in the image array are stored in a seperate array so that they can be easily recalled.

There are some changes to PEEK and POKE if multiple images are used.

DEF SEG = VARSEG(ARRAY(0))

While this statment can remain the same, VARPTR(ARRAY(OFFSET%)) should be used. Because you are not starting at 0, the inital offset which was 4 to skip the image info size may now be much greater, skipping an entire image.

W% = ARRAY(OFFSET%) \ 8
H% = ARRAY(OFFSET% + 1)
PTR& = VARPTR(ARRAY(OFFSET%)) '// BE SURE PTR IS A LONG INTEGER
DEF SEG = VARSEG(ARRAY(0))
PEEK/POKE 1& * Y% * W% + X% + PTR& + 4
DEF SEG

I still added a four because PTR& was only set to the starting point of this image. I still had to skip the four byte image size data.

 3. USING TRANSPARENT COLORS W/OUT MASKING

Of course, once a programmer begins using sprites, the next step is transparent colors. The best known method of doing this is with a mask which is ANDed before the real image is ORed on top of it. I myself have never actually used this method. There is another method which is much slower, but uses less memory, and just as effective.

These are the basic steps
1. Determine the dimensions of an image you wish to put on the screen and its transparent color (the array and transparent color can be passed to a SUB)
2. GET the area of the screen where the image is to be placed into a new array.
GET (X%, Y%)-(X% + W% - 1, Y% + H% - 1), BACKGRND%
3. Determine the segment address of both images
4. Use PEEK and POKE to copy pixel per pixel the source image into the BACKGRND% array. If the pixel is the transparent color, do not copy it.
5. PUT the final BACKGRND% array onto the screen with the PSET argument (PUT (X%, Y%), BACKGRND%, PSET)

Example:

*******************************************************
SUB (TRANSCOLOR%, X%, Y%, SOURCE() AS INTEGER, OFFSET%)
W% = SOURCE(OFFSET%) \ 8 '// Get dimensions of the source image
H% = SOURCE(OFFSET% + 1)
PTR& = VARPTR(SOURCE(OFFSET%)) + 4 '// advance to OFFSET% in bytes and skip four byte size info

DIM BACKGRND%(1& * W% * H% \ 2 + 3)
GET (X%, Y%)-(X% + W% - 1, Y% + H% - 1), BACKGRND%(0)
'// THIS IS THE AREA WHERE THE SOURCE IMAGE WILL GO

SSEG% = VARSEG(SOURCE(OFFSET%)) '// SEGMENT OF THE SOURCE IMAGE
BSEG% = VARSEG(BACKGRND%(0)) '// SEGMENT OF THE BACKGROUND IMAGE

FOR Y% = 0 TO H% - 1
FOR X% = 0 TO W% - 1
P& = 1& * Y% * W% + X% '// OFFSET USED FOR PEEK/POKE
DEF SEG = SSEG% '// Source segment address to ready computer for PEEKing
C% = PEEK(P& + PTR&)
IF C% <> TRANSCOLOR% THEN '// if it's not the transparent color then copy it to background.
DEF SEG = BSEG% '// Backgrnd% segment address to ready computer for POKEing
POKE P& + 4, C% '// don't forget to skip image size info
END IF
NEXT X%
NEXT Y%

DEF SEG
PUT (X%, Y%), BACKGRND%(0), PSET
ERASE BACKGRND% '// FOR MEMORY MANAGEMENT
END SUB
*******************************************************

This is only the beginning. With this type of manipulation you could rotate & scale image arrays. You could flip, mirror, change colors, etc. The possibilites go on.

 4. STORING AND RETRIEVING GRAPHICS FROM FILES

It is quite simple to store and retrieve several graphic images from a single file.

Saving:
OPEN FILENAME\$ FOR BINARY AS #1
FOR I% = 0 TO UBOUND(ARRAY)
PUT #1, , ARRAY(I%)
NEXT
CLOSE

OPEN FILENAME\$ FOR BINARY AS #1
REDIM ARRAY(LOF(1)) AS INTEGER
FOR I% = 1 TO LOF(1)
GET #1, , ARRAY(I% - 1)
NEXT
CLOSE

That's it! That's the secret everyone always wonders about. Now, of course, that was a very primitive example. Usually, you would want some kind of header which explains the number of images, and perhaps their offset in the file.

PUT #1, 1, NUMBEROFIMAGES%
PUT #1, , OFFSETOFIMAGE1&
PUT #1, , OFFSETOFIMAGE2&
etc...

Then just SEEK #1, OFFSETOFIMAGEx% and read it in. Of course, you'll have to know what size the image is. That can also be included at the top of the file. I have explained a file format I used in the program GFXDEMO.BAS in DIGGIF.ZIP
It is quite useful and very well commented. It makes it easy to store several graphic images plus a palette in a single file.