Issue 4-2, January 13, 1999

Be Engineering Insights: An Amusing Diversion, or How to Make Magnify Suck Up even More CPU Cycles

By Robert Chin

A while ago we received a bug report that included scanned-in polaroids of an engineer's BIOS screen and serial card. Pretty twisted—but we had fun using Magnify to examine the pictures. The only problem was that the image that Magnify showed was pixelated. The solution: Magnify filter add-ons.

This article shows you how to add add-on functionality to Magnify. It should also work as a tutorial for adding similar functionality to your own app. Add-on recognition is simple to incorporate, and it's a great way to extend, or have others extend, the functionality of an application.

To follow this article, open the sample source for Magnify and copy and paste. Note that all of the image-manipulation work is done in the add-ons themselves; Magnify just calls the add-on's functions to process the image.

We start by adding a couple of global variables to the app. The variables point to the filter's entry point function (gFilterFunc) and image id (gAddOnImageID). The function takes a BBitmap object; this is the image that the filter will work on:

void (*gFilterFunc)(BBitmap*);
image_id gAddOnImageID;

We initialize the variables in the TWindow constructor:

TWindow::TWindow(int32 pixelCount)
  : BWindow( BRect(0,0,0,0), "Magnify", B_TITLED_WINDOW,
    B_OUTLINE_RESIZE)
{
  gFilterFunc = 0;
  gAddOnImageID = 0;
  ...
}

We also create a global function that examines Magnify's add-on directory (a subdirectory of the default add-on directory). We create a menu item for each file we find (each file is assumed to be a Magnify filter):

static void
AddFilters(BMenu *menu)
{
  BPath path;
  find_directory (B_USER_ADDONS_DIRECTORY, &path);
  path.Append ("Magnify");

  BDirectory dir(path.Path());

  //  only add the rest of the menu if filters exist
  if (dir.CountEntries()
    return;

  menu->AddSeparatorItem();

  BMessage* msg = new BMessage('filt');
  msg->AddString("filter", "disable");
  BMenuItem* menuItem =
    new BMenuItem("Disable Current Filter", msg);
  menu->AddItem(menuItem);

  int32 index=1;
  entry_ref ref;

  while (true) {
    status_t err = dir.fffffffffGetNextRef(&ref);
    if (err == B_ENTRY_NOT_FOUND)
      break;
    else {
      msg = new BMessage('filt');
      msg->AddString("filter", ref.name);
      menuItem = new BMenuItem(ref.name, msg);
      menu->AddItem(menuItem);
      index++;
    }
  }
}

AddFilters() is called alongside the app's other menu-building functions, in TInfoView's AddMenu() function:

void
TInfoView::AddMenu()
{
  fMenu = new TMenu(dynamic_cast(Window()), "");
  BuildInfoMenu(fMenu);
  AddFilters(fMenu);
  ...
}

When the user selects a filter from the menu, TWindow::MessageReceived() catches the in-coming "load filter" message (which contains the name of the filter) and passes it to TWindow::LoadFilter():

void
TWindow::MessageReceived(BMessage* m)
{
  ...
    case 'filt':
      LoadFilter(m);
      break;
  ...
}

In TWindow::LoadFilter(), we reconstruct the pathname to the filter, and load it by passing the pathname to load_add_on() (first unloading the previous filter). We then call get_image_symbol() to find the add-on's filter() function—this is our interface to the add-on's functionality:

void
TWindow::LoadFilter(BMessage* m)
{
  char* fname;
  m->FindString("filter", (const char**)&fname);
  if (fname) {

    if (strcmp(fname, "disable") == 0) {
      gFilterFunc = 0;
      gAddOnImageID = 0;
      return;
    }

    //  unload the previous add-on, free its memory, etc.
    if (gFilterFunc)
      unload_add_on(gAddOnImageID);

    BPath path;
    find_directory (B_USER_ADDONS_DIRECTORY, &path, true);
    path.Append ("Magnify");
    path.Append(fname);

    //  load the add-on
    gAddOnImageID = load_add_on(path.Path());
    if (gAddOnImageID < 0) {
      printf("ERROR: can't load addon\n");
      return;
    }

    //  get the filter function symbol
    int result = get_image_symbol(gAddOnImageID, "filter",
      B_SYMBOL_TYPE_TEXT,(void**)&gFilterFunc);
    if (result < 0) {
      printf("ERROR: can't get image symbol\n");
      return;
    }
  }
}

The filter code is now loaded, and gFilterFunc points to its filter() function. To call the function, all we have to do is pass it the bitmap that's displayed on screen and render the result. We do this from the TOSMagnify::CreateImage() function, just before the DrawBitmap() call:

bool
TOSMagnify::CreateImage(BPoint mouseLoc, bool force)
{
...
      if (gFilterFunc)
        (*gFilterFunc)(fBitmap);

      DrawBitmap(fBitmap, srcRect, destRect);
...
}

What would add-on support be without some sample add-ons. Here is some sample source for a couple filters (thanks to Victor Tsou for coming up with this idea and creating both of these filters).

This first filter (continuously) rotates the Magnify image.

#include <math.h>
#include <stdio.h>
#include <Bitmap.h>

extern "C" _EXPORT void filter(BBitmap* image);

BRect bounds;
uint32 *bits = NULL;
int *xcosT, *xsinT;
int frame = 30;

#define max(a,b) (((a)>(b))?(a):(b))

#define PI 3.14159265
#define SCALE 1

void
filter(BBitmap* image)
{
  frame = (system_time() / 200000) % 360;

  if (bits && (bounds != image->Bounds())) {
    delete bits; bits = NULL;
    delete xcosT; xcosT = NULL;
    delete xsinT; xsinT = NULL;
  }

  if (!bits) {
    int32 m;
    bounds = image->Bounds();
    bits = new uint32[(image->BytesPerRow() / 4) *
      (bounds.Height() + 1)];
    m = max((int32)bounds.Width(), (int32)bounds.Height());
    xcosT = new int[m];
    xsinT = new int[m];
  }

  // prepare for 27.5 fixed point
  float sinT = 32*sin(frame*PI/180*SCALE),
        cosT = 32*cos(frame*PI/180*SCALE);

  int center_x = image->BytesPerRow()/8,
      center_y = (int32)bounds.Height()/2;
  int shiftx = (int)(-center_x*cosT - center_y*sinT +
                 center_x*32),
      shifty = (int)(center_x*sinT - center_y*cosT +
                 center_y*32);

  int newx, newy;
  int32 *dest_int32 = (int32 *)bits,
        *bitmap_bits_int32 = (int32*)image->Bits();
  int width = image->BytesPerRow() / 4,
     height = (int)image->Bounds().Height() + 1;

  for (int z=((width > height)?width:height)-1;z>-1;z--) {
    xcosT[z] = (int)(z*cosT); xsinT[z] = (int)(z*sinT);
  }

  for (int y=0;y<height;y++) {
    int ysinT = (int)(y*sinT), ycosT = (int)(y*cosT);
    int basex = ysinT + shiftx + (3*width << 5),
      basey = ycosT + shifty + (3*height << 5);
    for (int x=0;x<width;x++) {
      newx = ((basex + xcosT[x]) >> 5) % width;
      newy = ((basey - xsinT[x]) >> 5) % height;
      *(dest_int32++) =
        bitmap_bits_int32[newx + newy*width];
    }
  }

  memcpy(image->Bits(), bits,
    image->BytesPerRow() * (int)(bounds.Height()));

  frame++;
}

The second filter makes the image wave. Just build and run it, the visual is better than a description.

#include <math.h>
#include <stdio.h>
#include <Bitmap.h>

extern "C" _EXPORT void filter(BBitmap* image);

BRect bounds;
uchar *tbits;
int32 bpr;

#define PI 3.14159265

void
filter(BBitmap* image)
{
  int32 w, h, x, y;
  uchar *bits;
  int32 bpp;

  int frame = (system_time() / 20000) % 360;

  if (tbits && ((bounds != image->Bounds()) ||
                (bpr != image->BytesPerRow()))) {
    delete tbits; tbits = NULL;
  }

  if (!tbits) {
    bounds = image->Bounds();
    tbits = new uchar[image->BytesPerRow()];
    bpr = image->BytesPerRow();
  }

  w = (int32)bounds.Width() + 1;
  h = (int32)bounds.Height() + 1;
  bits = (uchar *)image->Bits();

  switch (image->ColorSpace()) {
    case B_RGB32 :
    case B_RGB32_BIG :
      bpp = 4; break;
    case B_RGB16 :
    case B_RGB16_BIG :
    case B_RGB15 :
    case B_RGB15_BIG :
      bpp = 2; break;
    case B_CMAP8 :
      bpp = 1; break;
    default :
      printf("Unknown color space (%x)\n",
        image->ColorSpace());
      return;
  }

  for (y=0;y<h;y++) {
    int32 delta = (int32)
        (30 * sin((10 * frame + y) * PI/180) +
         15 * sin(( 7 * frame + 3 * y) * PI/180));
    uchar *s;

    s = bits + y * image->BytesPerRow();

    if (delta < 0) {
      delta = -delta;
      memcpy(tbits, s, bpp * delta);
      memcpy(s, s + bpp * delta, bpp * (w - delta));
      memcpy(s + bpp * (w - delta), tbits, bpp * delta);
    } else if (delta > 0) {
      memcpy(tbits, s, bpp * (w - delta));
      memcpy(s, s + bpp * (w - delta), bpp * delta);
      memcpy(s + bpp * delta, tbits, bpp * (w - delta));
    }
  }
}

To build these filters, create a BeIDE "SharedLibrary" project (one project for each filter), and move the results to /boot/home/config/add-ons/Magnify. Name the first filter "Rotate" and the second "Wave". Next time you launch Magnify, you'll find the new menu items at the bottom of the pop-up list. Select a filter and watch the image.

What's next? How about combining filters and adding a game to Magnify.


Be Engineering Insights: Useful Applications of BeOS Scripting

By Jeff Bush

Contrary to its name, BeOS message scripting doesn't refer to a scripting language such a Perl or Awk. BeOS scripting is a format for passing messages between applications. Your programs already support scripting as many classes in the Interface Kit have it built in. The really interesting aspect of scripting is that applications can inter-operate without necessarily having to be specially designed to work together. The sample app, Thesaurus demonstrates this principle. You can download it from here:

<ftp://ftp.be.com/pub/samples/application_kit/Thesaurus.zip>

Thesaurus lets you scan through text that's displayed by some other application, and replace words with their synonyms. Ok, it's not totally useful (unless you're writing a college paper maybe!), but it is mildly entertaining, and the code could easily be adapted to work as a spelling checker or similar application. The synonyms are taken from a file that's included in the optional/goodies folder on the R4 CD.

The basic operation of this application is relatively simple. BTextView has a property named "Text" which gives you access to the contents of the text view. This application reads some text, scans for a word that it has synonyms for, presents the list of choices to the user, and then sets the text depending on what the user chooses. Unfortunately, the text view scripting messages used by Thesaurus don't yet belong to a suite, so if you try this with a program such as Eddie or BeIDE, you may find that it doesn't work.

One problem I ran into was how to find out which application Thesaurus should talk to. An easy way to do it was to allow the user to drag an icon onto the app. The message that is dragged is just bogus. When the view in question receives it, it won't know how to handle it and will reply with B_MESSAGE_NOT_UNDERSTOOD. The returned message, however, will have a messenger to the view in question which we can use to talk to the app. This worked fine for BeMail, but unfortunately StyledEdit doesn't accept dropped messages. To solve this problem, I've added optional command line apps that can be used to manually select a target. In the case of StyledEdit, you can use the optional command line arguments as follows:

Thesaurus --app StyledEdit --window 0 --view text

The meat and potatoes of Thesaurus is in the RemoteTextScanner class. It handles communication with the targeted text view. The actual work for getting the text is done in FetchNextTextChunk(), which fetches 1k of text from the targeted text view. BTextView has a property called "Text" which represents text that it contains. You can use an index specifier to choose which text you get back. The following code retrieves a chunk of text that starts at fTextBufferOffset and is TEXT_BUFFER_SIZE long:

BMessage textRequestMessage(B_GET_PROPERTY);

textRequestMessage.AddSpecifier("Text", fTextBufferOffset,
  TEXT_BUFFER_SIZE);

fTextViewMessenger.SendMessage(&textRequestMessage, &reply);

if (reply.FindString("result", &text) == B_OK)
  strncpy(fTextBuffer, text, TEXT_BUFFER_SIZE);

That's it. Replacing the targeted word is almost as easy. In this case, we change the verb to B_SET_PROPERTY. It is then a two step process of removing the old text and inserting the new:

// Erase the word that is going to be replaced
BMessage reply;
BMessage textDelMessage(B_SET_PROPERTY);

textDelMessage.AddSpecifier("Text", fCurrentWordOffset,
  fCurrentWord.Length());

fTextViewMessenger.SendMessage(&textDelMessage, &reply);

// Now insert the new word beginning at the same location
BMessage textSetMessage(B_SET_PROPERTY);

textSetMessage.AddString("data", newWord);
textSetMessage.AddSpecifier("Text", fCurrentWordOffset,
  strlen(newWord));

fTextViewMessenger.SendMessage(&textSetMessage, &reply);

The BTextView also has a "selection" property, which, as you might guess, allows us to change what text is selected. My plan was to set the selection to the currently selected word, so the user could see it in its context. This is where I ran into a little snag. It turns out that when the window containing a text view doesn't have the focus, the selection isn't highlighted. Since the thesaurus window has the focus when you're changing words, this won't work.

Luckily, the text view also has a property called "text_run_array". This property contains raw information about the font and color of text in the view, so it was a simple matter of setting the color of the word to red to indicate which word is currently being scanned. Note that this only works when multiple colors/fonts are enabled for the view, however, in most cases where you would want to use this app, they are.

There's quite a bit of room for improvement in this application: You could modify it to work within a selection by getting the selection property from the text view before starting, or improve the UTF8 support (BTextView does utilize UTF8, so fixing this should be simple).

I hope this gets your creative juices flowing in thinking of ways to make applications more scriptable. The scripting mechanism is still evolving, so if you have ideas for new properties or suites, you can submit requests using the bug database.


Developers' Workshop: A Stitch in MIME

By Eric Shepherd

Sometimes when you're writing an application that reads data files, it makes a difference what type of data the file contains. The BeOS uses MIME types to describe the type of data contained in files. For example, ASCII text files are type text/plain, and HTML files are text/html.

Sometimes applications try to read these files but the type information isn't correct. The most common case of this occurs when a file has been archived using the "tar" utility, then unarchived. This utility doesn't preserve attributes such as the file's type, so the file is untyped.

If an application is trying to read the file and interpret its contents, this can complicate matters. For example, a web server application (such as, say, the PoorMan web server that comes with the BeOS) needs to be able to tell the web browser receiving a file what type of file it is.

Here's some code that looks up the MIME type of a file whose name is given by filename:

BFile file;
char type[256];
status_t status;

status = file.SetTo(filename, B_READ_ONLY);
if (status == B_OK) {
  BNodeInfo ninfo(&file);
  if (ninfo.GetType(type)
    printf("Oh no! Can't get the type!\n");
  }
  else {
    printf("Type is %s\n", type);
  }
}

This code just uses the BNodeInfo::GetType() function to get the MIME type of the file, and fails if the function returns an error (which typically indicates that the file doesn't have type information established).

You could write code to look at the file's extension and/or contents and figure out what type of data it contains, but this isn't an optimal solution because your application shouldn't have to be aware of every possible type of data the file might contain.

The BeOS provides a function, update_mime_info(), that lets you ask the BeOS to attempt to identify the type of data in a file and update the file's type information.

Here's an altered version of the example code that uses update_mime_info() to try to have BeOS identify the file's type:

BFile file;
char type[256];
status_t status;

type[0] = '\0';
status = file.SetTo(filename, B_READ_ONLY);
if (status == B_OK) {
  BNodeInfo ninfo(&file);
  if (ninfo.GetType(type) < B_OK) {
    update_mime_info(filename, 0, 1, 0);
    if (ninfo.GetType(type) < B_OK) {
      printf("Can't get the type!\n");
    }
  }
  if (strlen(type)) {
    printf("Type is %s\n", type);
  }
}

This version, if GetType() fails, calls update_mime_info() to ask the BeOS to attempt to identify the file. Once that's been done, GetType() is called again to see if the type has been fixed. If not, an error message is displayed.

Let's look at update_mime_info() in more depth. Here's the prototype:

int update_mime_info(const char *path,
  int recursive,
  int synchronous,
  int force);

The path argument indicates the pathname of the file to update.

The recursive argument indicates that if pathname indicates a directory, whether or not the entire directory tree at that point should be traversed, updating every file in the tree. If this value is 1, the tree is traversed recursively; if it's 0, only the indicated file is updated.

To ask update_mime_info() to operate synchronously (so it will only return when it's finished updating), specify 1 for the synchronous parameter. Specify 0 if you want update_mime_info() to perform in the background; in this case, it returns immediately.

If the force argument is 1, files are updated even if they already have type information specified. If it's 0, files with types assigned already are skipped.

update_mime_info() returns a B_OK if all is well, otherwise it returns a negative value indicating the error that occurred.

If you haven't guessed already, this one function does at least 50% of the work of the mimeset utility. Most of the other 50% of the work is done by create_app_meta_mime(), which updates application meta-MIME information (it's called if the -apps or -all flag is specified to mimeset).

This may be helpful if you run into situations in which your application is trying to read files and is being confused by nonexistent type information.


The Next BeDC

By Jean-Louis Gassée

On April 9 and 10, we'll hold our next Be Developer Conference in Palo Alto. Last year, the event spotlighted the first release of the BeOS for Intel-based PCs. This year we're on to our third Intel-based release (or fourth or fifth, depending on how you count 3.1, 3.2, 4.0 and the upcoming 4.1) and, indeed, we'll be happy to chat about the progress we've made in our UI, the kernel, the file system, etc. But at the April conference we're going to focus on the Media Kit, one of Release 4's most notable additions to the BeOS.

Given our Media OS banner, and our plans for a media-rich future for the Be commonwealth, this topic is clearly rich enough for several Developer Conferences. Using the Media Kit, and using it well, is essential to apps that want to be fast and flexible.

So, does this mean Be is only interested in Media Kit-based applications? I'll just summarize earlier statements made in this space: Media applications are the ones that are most likely to help distinguish the BeOS from its venerable elders. But drawn by media, BeOS users need to be delighted enough with our "everyday" apps—e-mail, Web browsing, word processing, spreadsheet—that they'll find no reason to leave.

Prior to the April BeDC we'll promote the BeOS Media Kit at events such as the NAMM Music Market in Los Angeles, January 28-31, the Frankfurt Musikmesse, March 3-7, and the CeBIT show in Hannover, March 18-24. Tim Self and his team expect a number of announcements for these events; these announcements will provide good technical (and business) cases, which we'll use to encourage more developers to embrace our technology.

In June we're off to PC Expo. Last year was the first time Be took part in a PC-centric trade show. This year we'll return with a proof of the concept that we demonstrated in 1998. This represents a lot of work -- but this is fun work, and it's paying off. The Release 4 sales results so far are a gentle but firm tug that indicates traction: Distributors are paying their bills and ordering more. This motivates all of us for the next milestones and we all look forward to a fun and productive BeDC.

Creative Commons License
Legal Notice
This work is licensed under a Creative Commons Attribution-Non commercial-No Derivative Works 3.0 License.