Saturday, April 26, 2014

Remembering PEEK and POKE

Remember PEEK and POKE? Back in the Precambrian (or pre-PC) era of computing, the primary way to program the Apple and TRS-80 and Commodore machines was with BASIC. And really the only way to do the cool stuff was to get in and diddle system memory and hardware registers. The way to read memory was PEEK int, which returned the value of the byte at int. And with POKE you could store a value into a byte of memory. Ahhh... good times...

Well, crazily enough, PyQt offers an equivalent: a way to get direct access to data in memory. It's called a voidptr and is a type defined by SIP, PyQt's interface to the C++ world of Qt. You can get a voidptr from PyQt wherever the Qt documentation says a method returns uchar *. Such places are not common, but one emerged when I first implemented PPQT's scan image display.

A major feature of PPQT is that it shows the scan image from which the book's text was OCR'd, alongside the text itself in the editor window. As you move the edit cursor from page to page, the image viewer tracks it, flipping from scan image to image. It's one feature that PPQT has over its predecessor Guiguts, which requires the user to have a separate image-display app.

Here is the V2 imageview undergoing unit test. As you see it offers an adjustable zoom. Sometimes, especially when proofing Greek or a small-print footnote, the user needs to zoom in and peer closely. But usually you just want the whole page visible so you can scan for italics and bold, or check hyphenations.

The To-Width and To-Height buttons are supposed to set the zoom automatically so that the printed part of the page just fills the window side-to-side or top-to-bottom. When I first implemented these buttons back in V.1 I found it quite the coding challenge.

Here's what To-Width has to do:

  1. Scan the pixels of the image to find the width of the nonwhite area.
  2. Get the ratio of that width to the image's viewport width.
  3. Set that ratio as the zoom factor and redraw the image.
  4. Set the scroll position of the scroll area so as to center the nonwhite block.

Implementing these steps took me into back-alleys of Qt where I'd never been before, and introduced me to the SIP voidptr. In order to do step 1, I have to inspect the pixels of the image row by row, looking for the left and right edges of the text. I make sure the image is in the 8-bit Indexed Color mode, so that each pixel is one byte. Then the method QImage.bytes() returns a uchar * pointing to the bytes/pixels that comprise the image. The PyQt translation of uchar * is SIP.voidptr.

You can't use the voidptr as returned. First you must define the amount of memory it represents:

    vp = my_qimage.bytes()
    vp.setsize(my_qimage.width() * my_qimage.height())

Now that PyQt knows the bounds of the addressed memory—and one wonders: does it check the size; or would it let you define a very large size and potentially examine things you shouldn't?—anyway once you have set the bounds, you can index the voidptr as if it were a Python bytes string:

    if vp[j] < b'\xff': # nonwhite pixel

Which is exactly PEEK j! plus ça change, plus c'est la même chose...

Next post: performance tuning, or how fast can you finger the pixels?

No comments: