Each release of the BeOS includes some new APIs (such as the Media Kit in
R4), along with a bunch of minor to major API "improvements" (or so we
like to think :-). You'll notice quite a few "extras" in the R4 BFont
class, many of them inspired by feature requests from developers (thanks
for taking the time to send them).
The SetFace
(uint16 face
)
and uint16 Face()
APIs have been around for
awhile, but they haven't really been supported. We initially chose to
define a font by its family name (a free-form string, universally used)
and its style name (another free-form string, far less common). The main
advantage of this was that such a name could be extended without limits.
But this turned into a disadvantage, as the lack of a standard implied
that the style name and the family name could not be set as two
orthogonal options, because there was no easy way to know what styles
would be found in the given font's family.
The font face provides such an orthogonal choice by defining the set of possibilities once for all, based on the OS/2 TrueType table standard: italic, underscore, negative, outlined, strikeout, bold, and regular. The first six can be freely combined (they are defined as one bit in a bit mask). Regular is reserved for the standard appearance of the font and can't be mixed with any of the other ones.
For completeness, SetFamilyAndStyle()
has been extended into
SetFamilyAndFace
(font_family family
, uint16 face
).
Finally, setting a
face operates by the closest match. If a perfect match isn't available,
there will be no emulation of the missing attributes. So for example, if
you ask for Bold StrikeOut but the proper font is not installed, then you
may just get Bold. The system will not strike it out for you. That may be
improved in future releases.
In R4, we enabled support for Postscript Type 1 fonts. So you may now
want to identify the file format used by a given font family:
font_file_format FileFormat()
,
which returns B_TRUETYPE_WINDOWS
or
B_POSTSCRIPT_TYPE1_WINDOWS
. Every time the font file manager updates your
list of installed font files, it sorts them by family, creating a list of
available styles per family. You can't do this across different file
formats, though. So if, for example, you have Baskerville Regular and
Baskerville Bold in TrueType and Baskerville Italic in Postscript, you'll
see two families: Baskerville(TT) with two styles and Baskerville(PS)
with one style. This is required, as different sets of font files sharing
the same family name don't always share the same exact design and metric.
Also, even if Postscript Type 1 fonts are transparently supported by the BeOS font system, we don't recommend using them as regularly as TrueType fonts. First, because of their limited hinting, they usually render poorly at small sizes, so it's best to avoid them for screen display. Second, our TrueType engine's performance is still significantly better and remains the best choice when font rendering speed is critical. Last, because of their limitation in character encoding, you may see problems with non-ASCII 7-bit or special characters. The current compatibility with UTF8 is limited, and it's not clear what future improvements can be made, if any at all.
Many people rightly complained that there was no easy way—if any way at all—to know which glyphs were available in a given font. The system would transparently return the expected glyph or the default box, with no complaints. As part of our increased support for localized/international apps, we created the unicode_block class. This object is basically a 128-bit bitfield, capable of handling basic logic operations (AND and OR), tests (EQUAL and NOT EQUAL), and the more sophisticated "Includes".
The Unicode™
range has been cut into 70 blocks (see list in
UnicodeBlockObjects.h
), and
new blocks may be added in the future. For
example, you'll find the basic ASCII 7-bit
block, B_BASIC_LATIN_BLOCK
(Unicode values 0x0000 to 0x007F), and more exotic ones like
B_TIBETAN_BLOCK
(0x0F00 to 0x0FBF).
Blocks()
returns a mask of all
unicode_blocks that are even partially available in a given font. So for
example, a simple test like:
if (aFont
->Blocks
().Includes
(B_TYPICAL_JAPANESE_BLOCKS
))
where B_TYPICAL_JAPANESE_BLOCKS
is a bitmask (yet to be defined) that
contains all the regular Japanese unicode_blocks, indicates that
aFont
should include most common Japanese characters (though specific glyphs
may be missing). It's what a text editor needs to switch fonts
dynamically when the user switches between different input methods
(Hiroshi will give more details about this in a future article).
For people who want to go even further and know if a specific glyph is
available, GetHasGlyphs()
is what you need.
Note: The APIs discussed here are not very well supported for PS Type 1
fonts. Blocks()
may return approximated
results, and GetHasGlyphs()
is not
currently supported at all.
The new GetGlyphShapes()
function returns the shape(s) of one or more
glyph(s), described using Bézier curves and lines. The shapes have their
origin at (0.0, 0.0)
to allow easy linear transformation. To draw them at
the same position that a DrawString()
would, you need to offset them, using
the following formula:
OffsetBy
(floor
(pen0
.x
+0.5),floor
(pen0
.y
+0.5)-1.0);
also written as:
OffsetBy
(floor
(pen0
.x
+0.5),floor
(pen0
.y
-0.5));
is the rounding rule used by the app_server. The offset -1.0
on the Y axis is required to compensate for a historical mistake, now
tied to the API forever (for compatibility reasons): the bitmap images
generated by the font engine use an origin in floor
(+0.5)(0.0, -1.0)
, one line over
the normal baseline of the font. So all font drawing is done one line
higher than expected. Since all texts are placed so that they look good,
no one has noticed or complained about this during the last 18 months.
All font-related APIs take that error into account, with the exception of
this one and the font-wide bounding box (see the next paragraph), which
both describe linearly scalable objects, and must originate at (0.0, 0.0)
.
How do we know where a DrawString()
is going to draw and what area we
should erase to get a correct refresh?
Until R4, the only way to figure this out was by using approximated rules based on the escapements of the glyphs; the ascent, descent, and leading of the font, along with empirical safety margins. Such solutions had two serious flaws. First, the ascent, descent, and leading of the font give detailed measurements of how tall a text line should be, but don't guarantee that the font designer didn't intentionally create glyphs so tall that they will infringe outside their text line. Second, the escapement does give a measurement of how much the pen position will move after drawing the glyph, but doesn't guarantee anything about where the glyph is really going to be drawn.
That's why R4 introduces new APIs, to allow efficient and accurate
processing of those bounding boxes. The first function is global to a
font: BRect
BoundingBox()
.
If you draw all the glyphs of a given font and
size, one on another, you get a big blob. BoundingBox()
is the bounding
box of that blob. It's a floating-point scalable rectangle, the value
returned corresponding to point size 1.0. By scaling it, you can get good
approximations of the global font's printing bounding box at print time.
Sadly, when you draw on screen, you don't get the x4, x8, or x16 resolution increase of a printer before rounding to an integer. Also at small sizes, hinting may try to reduce readability problems by distorting glyphs. As a result, calculating the screen- approximated bounding box for a given point size, based on the font's real bounding box, is a non-trivial task. This gets even worse when you consider that around 20% of the font files out there provide incorrect bounding box information for the left side (don't ask me why!). As a result of extensive tests that I ran on hundreds of common and less common fonts, I propose the following formulas:
fBBox
is the BoundingBox()
BRect
. Please note that the Y axis is
oriented from bottom to top, as defined in font files. It must be
inverted to be used in a standard screen or printer coordinate system.
pSize
is the selected point size.
sBBox
is the estimated screen bounding box. All borders are included.
sBBox
.left
=floor
(fBBox
.left
*pSize
)-1;sBBox
.top
=floor
(-fBBox
.bottom
*pSize
)-1;sBBox
.right
=floor
(fBBox
.right
*pSize
)+2;sBBox
.bottom
=floor
(-fBBox
.top
*pSize
)+2;
These formulas work fine with the 80% of font files that include accurate bounding box info. Fonts provided by operating system vendors seem to be always compliant.
sBBox
.right
=floor
(fBBox
.right
*pSize
* 1.333)+2;
This formula solves the problem with 90% of the "bad fonts."
sBBox
.right
=floor
(fBBox
.right
*pSize
* 2.0)+2;
This works with all the fonts I tested, except one that was also wrong on
the Y axis (I wonder why they cared about setting the bounding box
information at all...). That global bounding box allows you to calculate
an estimate of the drawing area used by DrawString()
.
total_escape
: sum of the escapements of all the glyphs of the string,
from the first one (included) to the last one (excluded).
drawRect
: resulting bounding box for the
DrawString()
.
pen0
: pen position when calling DrawString()
.
As sBBox
is a scaled version of the theoretical font bounding box, we
have to take the -1.0 correction for bitmap fonts into account
ourselves (that's the only other case with GetGlyphShapes()
).
drawRect
=sBBox
.left
;drawRect
.right
+=total_escape
;drawRect
.OffsetBy
(floor
(pen0
.x
+0.5),floor
(pen0
.y
-0.5));
The validity of drawRect
will be as good as
that of sBBox
(as discussed
earlier). Most of the time, it won't be the best value, but will be many
pixels wider and taller. Its advantage, though, is that it can be
processed completely on the client side by caching only one rectangle by
font (be careful, BoundingBox()
is not cached, so it will make a
synchronous call to the server).
For applications where performance isn't critical, other APIs allow you
to calculate the minimal bounding box of a glyph or a string, either on
the screen (the rectangle is rounded to an integer and takes all
distortions into account) or as printed (original floating-point values).
You can switch between the two modes by using the font_metric_mode
parameter (B_SCREEN_METRIC
or B_PRINTING_METRIC
).
The function exists in different flavors: bounding boxes for individual
glyphs (GetBoundingBoxesAsGlyphs()
) or whole words
(GetBoundingBoxesAsString()
for a single string, or
GetBoundingBoxesForStrings()
to process many strings in one call). To
convert those rectangles to screen coordinates, you need to apply the
regular offset formula (no -1.0 needed):
drawRect
.OffsetBy
(floor
(pen0
.x
+0.5),floor
(pen0
.y
+0.5));
Results are demonstrated in the new fontDemo application shipped with R4, by enabling the Bounding boxes option.
Some developers noticed an inconsistency in our
GetEscapements()
API. There
was no way to get the real 2D escapement when using a rotated font. Worse
than that, the 1D value returned in that case was wrong. One work around
was to get the non-rotated escapement and apply the rotation yourself,
but then rounding errors were unavoidable. As we want perfectly accurate
positions on screen to be possible, we created a new version of
GetEscapements()
that returns one
BPoint
per glyph.
Even beyond that, a few spacing modes (for example using dynamic kerning)
modify the real drawing origin of glyphs without changing their
escapements. For example, the width reserved for a glyph stays the same,
but it will be drawn a little more to the left to improve the overall
appearance of the string. So the most advanced version of GetEscapements()
returns two BPoint
s per glyph, one for the escapement, the other one for
the small drawing origin offset, if any.
The BFont
object knows about four different spacing modes. Those were
created to allow optimal font display on screen in different cases.
B_CHAR_SPACING
has been improved in R4 to reduce collisions between
characters at small point sizes. B_STRING_SPACING
was not behaving very
well in Release 3, so it was almost completely rewritten for R4. The new
dynamic kerning engine is much smarter than the previous one and now
protects spaces with great care. That makes it a clear winner if you want
nice WYSIWYG
text display only. It can also be used for text editing, but
that requires special, non-trivial processing to reduce jittering.
Contact me directly if you want more details...
Awhile ago, when we failed to meet our quality expectations with our
current fonts and B&W rasterizer, we chose to keep anti-aliasing always
enabled. So B_DISABLE_ANTIALIASING
was added for applications with
special needs. Recently, we experimented with an automatic way of
enabling/disabling anti-aliasing based on the point size and
font-specific information. The results were not satisfying, so the option
isn't in R4. The new flag B_FORCE_ANTIALIASING
, added for completeness,
was not removed but just disabled. More news in future releases.
B_TRUNCATE_SMART
has not been implemented yet... For now, it still
defaults to B_TRUNCATE_MIDDLE
.
BFont
::IsFullAndHalfFixed()
isn't a spelling mistake. Since Kanji
characters are much wider than most Roman characters, it's not reasonable
to create a fixed-width font with both Kanji and Roman glyphs. The
solution lies in designing Roman glyphs half the width of Kanji. Sadly,
we didn't have time to implement it for R4, and even less to add proper
support in system applications. But these things will come with time...
Long-time reader and BeOS fanatic Tim Dolan recently wrote to me
regarding the new BShape
and
BShapeIterator
classes. “From the header
file, I can see that the BShape
class is just what I need to draw smooth
curves,” Tim wrote. “But I need help getting started. Can you provide
some no-frills sample code which covers the basics?” Here you go, Tim:
ftp://ftp.be.com/pub/samples/interface_kit/Iterview.zip
This sample code shows how to get the outline of a text string as a
BShape
and manipulate the control points
of the BShape
through
BShapeIterator
, in order to distort the text.
Before you dig into the sample code, take a minute to examine the
Shape.h
header file. The BShape
class has four central functions, which are used
to describe a curve or path:
MoveTo()
moves to the specified point.
LineTo()
creates a line between the current point and the specified
point.
BezierTo()
describes a cubic Bézier curve which starts at the current
point and ends at the third point specified.
Close()
creates a line between the current point
and the first point of the BShape
.
The BShapeIterator
class has four corresponding
functions: IterateMoveTo()
,
IterateLineTo()
,
IterateBezierTo()
, and
IterateClose()
. An additional function,
Iterate()
, binds them all together by stepping
through each point of the given BShape
, calling the
appropriate Iterate...To()
function, and passing
it a pointer to the BPoint
or
BPoint
s which describe that segment of the path. For
this to be useful, you need to derive a class from the
BShapeIterator
, replacing the
Iterate...To()
functions with functions that do
something interesting, such as displaying the
BPoint
s or relocating them.
In the sample code, we start by creating the
IterView
class, which inherits from both the
BShapeIterator
class and the
BView
class. We'll override the
Iterate...To()
functions and have each one draw
the control points in the view. We'll also keep lists of the control points
(one list for each of the glyphs in the text string), which will allow the
MouseDown()
and
MouseMoved()
functions to manipulate the
BShape
.
We start by obtaining the outlines for the glyphs of our text in the
InitializeShapes()
routine:
voidIterView
::InitializeShapes
() {BFont
font
;GetFont
(&font
);font
.SetSize
(fontSize
);delta
.nonspace
= 0.0;delta
.space
= 0;font
.GetGlyphShapes
(text
,textlen
,shapes
);font
.GetEscapements
(text
,textlen
, &delta
,esc
,esc
+textlen
); }
We also get the escapement values for the text, which (when multiplied by the font size) lets us determine the placement of each glyph. This is important, as the coordinates of each glyph shape are in absolute terms, not relative to one another.
The Draw()
function is the heart of the matter, as
it calls the Iterate()
function of the
BShapeIterator
class. Each time through the loop,
the offset point is adjusted to determine the starting point of the glyph
shape:
voidIterView
::Draw
(BRect
R
) {BPoint
where
(initialPoint
); for (int32i
=0,curShape
=0;i
<textlen
;i
++,curShape
++) {offset
.Set
(floor
(where
.x
+esc
[i
+textlen
].x
+0.5),floor
(where
.y
+esc
[i
+textlen
].y
+0.5)-1.0);MovePenTo
(offset
);SetHighColor
(0,0,0);SetPenSize
(2);StrokeShape
(shapes
[i
]);SetPenSize
(1);Iterate
(shapes
[i
]);where
.x
+=esc
[i
].x
*fontSize
;where
.y
+=esc
[i
].y
*fontSize
; }; if(firstPass
)firstPass
=false
; }
For simplicity's sake, we're drawing directly to the view, rather than off-screen. This results in flicker when dragging a control point, and is certainly the first thing you'll want to take care of in a real application.
The first time you call the Iterate()
function,
the firstPass
flag is true
, and
each Iterate...To()
function adds the point or
points and the offset to a BList
of
glyphPts
associated with that
BShape
. For example:
status_tIterView
::IterateLineTo
(int32lineCount
,BPoint
*linePts
) {SetHighColor
(255,0,0); for(inti
=0;i
<lineCount
;i
++,linePts
++) {FillEllipse
(*linePts
+offset
, 2, 2); if(firstPass
) {shapePts
[curShape
].AddItem
(newglyphPt
(linePts
,offset
)); } }currentPoint
= *(linePts
-1)+offset
; returnB_OK
; }
Note that each Iterate...To()
function also sets
the currentPoint
, which
is the last point of the BShape
that was drawn.
The IterateBezierTo()
function needs a little
explanation regarding its control points. BShape
uses cubic Bézier curves, which means that the curve is described by
four control points; but note that BezierTo()
and
IterateBezierTo()
are given
BPoint
s in groups of three, not four. The first
control point is the point of the BShape
immediately
preceding the BezierTo()
call; this point is not
explicitly passed into BezierTo()
or
IterateBezierTo()
. The last control point is the
endpoint of the curve. The two intermediate control points determine the
shape and amplitude of the curve between the first and fourth control
points.
The MouseDown()
function starts by determining if
the mouse down point falls inside the bounding box of a
BShape
. If it does, it then searches the list of
glyphPts
associated with that
BShape
to determine if the mouse point is within a
small distance of one of the control points. During this search, we need to
take into account the offset of our BShape
. If a
point is found, we set the isTracking
flag to
true
, and set dragPoint
to the
selected control point.
If isTracking
is true
and we enter
the MouseMoved()
function, we change
the position of the chosen control point (once again taking into account
the offset) and Invalidate the view:
voidIterView
::MouseMoved
(BPoint
pt
, uint32code
, constBMessage
*msg
) { if(isTracking
==false
) return; *(dragPoint
->pt
) = (pt
-dragPoint
->offset
);Invalidate
(); }
There are many interesting things you can do with
BShapes
and BShapeIterators
.
Most obviously, they make excellent drawing primitives, which let you
accurately and flexibly describe complex curves. You can create wonderful
effects or even animations by applying various transformations to the
BShape
, especially for text. You can also clip to
BShape
s: just create a
BPicture
for your BShape
,
display an interesting pattern or bitmap, call
BView
::ClipToPicture()
, and you've got an amazing
effect for just a few lines of code.
Now that you've seen the possibilities, make it your New Year's
resolution to get into BShape
!
I spent some time in the Old Country over the holidays, seeing family and friends, visiting Mont Saint-Michel and the Louvre, and sharing the merriment at the birth of the Euro. While I was there, I also got to enjoy the feedback our work gets in Europe. As I write this, I've just heard from Jean Calmon that the BeOS got the top award from PC Expert, the French version of PC Magazine, coming in ahead of Windows 98 and DirectX. We appreciate the recognition, not least of all because it helps us further our gains with software developers, resellers, and end users.
Not that there aren't still skeptics, but this time they weren't humming the "Microsoft über alles" leitmotif. No, they were cheekier and a little more creative. One doubter offered to license his secret plan for going public in six months or less. Instead of continuing with the obsolete French Farmer business model, you build a business, make money, and then sell shares of future profits in order to finance growth and make your anticipated earnings real.
The plan goes like this: build a simple but tasteful Web site and use it to sell dollar bills, one at a time, for 70 cents each. Yes, he agrees, you'll lose money on each transaction, but think of the traffic you'll build! From that high-volume traffic, you'll generate revenues well above your transaction losses. Even taking postage and handling into account, you'll be profitable before the investment bankers can park their BMWs behind your office. Then you can have an old-fashioned IPO to finance further growth. You can vary the plan—sell stamps at less than face value, offer special editions for advertisers. The Web frenzy will keep the stock climbing post-IPO.
When I protested, I was chided for not seeing that this was equivalent to free e-mail. You provide something for less than it costs, but with better returns. Dollar bills scale up more easily than mail servers. They require little maintenance and users need no tech support.
Another (French) acquaintance had an explanation for eBay's greater-than 5000 P/E ratio. He described the following "virtuous" circle. In order to keep the stock price in the stratosphere, the founders sell a little stock, enough to buy everything that's not snapped up by eBay users. They put all the junk in containers and dump it in the Bay (hence the company name). Users are thrilled. The word of mouth is terrific and attracts more users. The stock keeps climbing and the loop is closed.
Now, there are a couple of professional flea markets around Paris, but garage sales aren't part of the culture. Merchants who need stuff to sell advertise that they'll come to your house and, as a service, empty your cellar and attic of all its bizarre and embarrassing junk. eBay, my wine bistro companion explained, performs a similarly valuable service by taking a modest amount of money from the stock market and using it as an incentive for people to clean their closets and garages. Everyone benefits from putting the stock market and cyberspace at the service of New Year resolutions.
My own resolution is never to appear to endorse such unseemly views of respectable industry trends. As we say in my adopted language, this is a game of confidence, or something like that. We cannot allow disparaging satire or infelicitous irony to undermine the glorious edifice of trust in the new business models.
More seriously, a very Happy New Year to all the members of the Be extended family, with our best wishes for personal and professional realization.