blob: 01f73e082f0ba4b0ba3c4d1b8cbe9e235f72694a [file] [log] [blame]
/*
* Copyright 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "ScreenRecord"
//#define LOG_NDEBUG 0
#include <utils/Log.h>
#include "TextRenderer.h"
#include <assert.h>
#include <malloc.h>
#include <string.h>
namespace android {
#include "FontBitmap.h"
};
using namespace android;
const char TextRenderer::kWhitespace[] = " \t\n\r";
bool TextRenderer::mInitialized = false;
uint32_t TextRenderer::mXOffset[FontBitmap::numGlyphs];
void TextRenderer::initOnce() {
if (!mInitialized) {
initXOffset();
mInitialized = true;
}
}
void TextRenderer::initXOffset() {
// Generate a table of X offsets. They start at zero and reset whenever
// we move down a line (i.e. the Y offset changes). The offset increases
// by one pixel more than the width because the generator left a gap to
// avoid reading pixels from adjacent glyphs in the texture filter.
uint16_t offset = 0;
uint16_t prevYOffset = (int16_t) -1;
for (unsigned int i = 0; i < FontBitmap::numGlyphs; i++) {
if (prevYOffset != FontBitmap::yoffset[i]) {
prevYOffset = FontBitmap::yoffset[i];
offset = 0;
}
mXOffset[i] = offset;
offset += FontBitmap::glyphWidth[i] + 1;
}
}
static bool isPowerOfTwo(uint32_t val) {
// a/k/a "is exactly one bit set"; note returns true for 0
return (val & (val -1)) == 0;
}
static uint32_t powerOfTwoCeil(uint32_t val) {
// drop it, smear the bits across, pop it
val--;
val |= val >> 1;
val |= val >> 2;
val |= val >> 4;
val |= val >> 8;
val |= val >> 16;
val++;
return val;
}
float TextRenderer::getGlyphHeight() const {
return FontBitmap::maxGlyphHeight;
}
status_t TextRenderer::loadIntoTexture() {
ALOGV("Font::loadIntoTexture");
glGenTextures(1, &mTextureName);
if (mTextureName == 0) {
ALOGE("glGenTextures failed: %#x", glGetError());
return UNKNOWN_ERROR;
}
glBindTexture(GL_TEXTURE_2D, mTextureName);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// The pixel data is stored as combined color+alpha, 8 bits per pixel.
// It's guaranteed to be a power-of-two wide, but we cut off the height
// where the data ends. We want to expand it to a power-of-two bitmap
// with ARGB data and hand that to glTexImage2D.
if (!isPowerOfTwo(FontBitmap::width)) {
ALOGE("npot glyph bitmap width %u", FontBitmap::width);
return UNKNOWN_ERROR;
}
uint32_t potHeight = powerOfTwoCeil(FontBitmap::height);
uint8_t* rgbaPixels = new uint8_t[FontBitmap::width * potHeight * 4];
memset(rgbaPixels, 0, FontBitmap::width * potHeight * 4);
uint8_t* pix = rgbaPixels;
for (unsigned int i = 0; i < FontBitmap::width * FontBitmap::height; i++) {
uint8_t alpha, color;
if ((FontBitmap::pixels[i] & 1) == 0) {
// black pixel with varying alpha
color = 0x00;
alpha = FontBitmap::pixels[i] & ~1;
} else {
// opaque grey pixel
color = FontBitmap::pixels[i] & ~1;
alpha = 0xff;
}
*pix++ = color;
*pix++ = color;
*pix++ = color;
*pix++ = alpha;
}
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, FontBitmap::width, potHeight, 0,
GL_RGBA, GL_UNSIGNED_BYTE, rgbaPixels);
delete[] rgbaPixels;
GLint glErr = glGetError();
if (glErr != 0) {
ALOGE("glTexImage2D failed: %#x", glErr);
return UNKNOWN_ERROR;
}
return NO_ERROR;
}
void TextRenderer::setProportionalScale(float linesPerScreen) {
if (mScreenWidth == 0 || mScreenHeight == 0) {
ALOGW("setFontScale: can't set scale for width=%d height=%d",
mScreenWidth, mScreenHeight);
return;
}
float tallest = mScreenWidth > mScreenHeight ? mScreenWidth : mScreenHeight;
setScale(tallest / (linesPerScreen * getGlyphHeight()));
}
float TextRenderer::computeScaledStringWidth(const String8& str8) const {
// String8.length() isn't documented, but I'm assuming it will return
// the number of characters rather than the number of bytes. Since
// we can only display ASCII we want to ignore anything else, so we
// just convert to char* -- but String8 doesn't document what it does
// with values outside 0-255. So just convert to char* and use strlen()
// to see what we get.
const char* str = str8.string();
return computeScaledStringWidth(str, strlen(str));
}
size_t TextRenderer::glyphIndex(char ch) const {
size_t chi = ch - FontBitmap::firstGlyphChar;
if (chi >= FontBitmap::numGlyphs) {
chi = '?' - FontBitmap::firstGlyphChar;
}
assert(chi < FontBitmap::numGlyphs);
return chi;
}
float TextRenderer::computeScaledStringWidth(const char* str,
size_t len) const {
float width = 0.0f;
for (size_t i = 0; i < len; i++) {
size_t chi = glyphIndex(str[i]);
float glyphWidth = FontBitmap::glyphWidth[chi];
width += (glyphWidth - 1 - FontBitmap::outlineWidth) * mScale;
}
return width;
}
void TextRenderer::drawString(const Program& program, const float* texMatrix,
float x, float y, const String8& str8) const {
ALOGV("drawString %.3f,%.3f '%s' (scale=%.3f)", x, y, str8.string(),mScale);
initOnce();
// We want to draw the entire string with a single GLES call. We
// generate two arrays, one with screen coordinates, one with texture
// coordinates. Need two triangles per character.
const char* str = str8.string();
size_t len = strlen(str); // again, unsure about String8 handling
const size_t quadCoords =
2 /*triangles*/ * 3 /*vertex/tri*/ * 2 /*coord/vertex*/;
float vertices[len * quadCoords];
float texes[len * quadCoords];
float fullTexWidth = FontBitmap::width;
float fullTexHeight = powerOfTwoCeil(FontBitmap::height);
for (size_t i = 0; i < len; i++) {
size_t chi = glyphIndex(str[i]);
float glyphWidth = FontBitmap::glyphWidth[chi];
float glyphHeight = FontBitmap::maxGlyphHeight;
float vertLeft = x;
float vertRight = x + glyphWidth * mScale;
float vertTop = y;
float vertBottom = y + glyphHeight * mScale;
// Lowest-numbered glyph is in top-left of bitmap, which puts it at
// the bottom-left in texture coordinates.
float texLeft = mXOffset[chi] / fullTexWidth;
float texRight = (mXOffset[chi] + glyphWidth) / fullTexWidth;
float texTop = FontBitmap::yoffset[chi] / fullTexHeight;
float texBottom = (FontBitmap::yoffset[chi] + glyphHeight) /
fullTexHeight;
size_t off = i * quadCoords;
vertices[off + 0] = vertLeft;
vertices[off + 1] = vertBottom;
vertices[off + 2] = vertRight;
vertices[off + 3] = vertBottom;
vertices[off + 4] = vertLeft;
vertices[off + 5] = vertTop;
vertices[off + 6] = vertLeft;
vertices[off + 7] = vertTop;
vertices[off + 8] = vertRight;
vertices[off + 9] = vertBottom;
vertices[off + 10] = vertRight;
vertices[off + 11] = vertTop;
texes[off + 0] = texLeft;
texes[off + 1] = texBottom;
texes[off + 2] = texRight;
texes[off + 3] = texBottom;
texes[off + 4] = texLeft;
texes[off + 5] = texTop;
texes[off + 6] = texLeft;
texes[off + 7] = texTop;
texes[off + 8] = texRight;
texes[off + 9] = texBottom;
texes[off + 10] = texRight;
texes[off + 11] = texTop;
// We added 1-pixel padding in the texture, so we want to advance by
// one less. Also, each glyph is surrounded by a black outline, which
// we want to merge.
x += (glyphWidth - 1 - FontBitmap::outlineWidth) * mScale;
}
program.drawTriangles(mTextureName, texMatrix, vertices, texes,
len * quadCoords / 2);
}
float TextRenderer::drawWrappedString(const Program& texRender,
float xpos, float ypos, const String8& str) {
ALOGV("drawWrappedString %.3f,%.3f '%s'", xpos, ypos, str.string());
initOnce();
if (mScreenWidth == 0 || mScreenHeight == 0) {
ALOGW("drawWrappedString: can't wrap with width=%d height=%d",
mScreenWidth, mScreenHeight);
return ypos;
}
const float indentWidth = mIndentMult * getScale();
if (xpos < mBorderWidth) {
xpos = mBorderWidth;
}
if (ypos < mBorderWidth) {
ypos = mBorderWidth;
}
const size_t maxWidth = (mScreenWidth - mBorderWidth) - xpos;
if (maxWidth < 1) {
ALOGE("Unable to render text: xpos=%.3f border=%.3f width=%u",
xpos, mBorderWidth, mScreenWidth);
return ypos;
}
float stringWidth = computeScaledStringWidth(str);
if (stringWidth <= maxWidth) {
// Trivial case.
drawString(texRender, Program::kIdentity, xpos, ypos, str);
ypos += getScaledGlyphHeight();
} else {
// We need to break the string into pieces, ideally at whitespace
// boundaries.
char* mangle = strdup(str.string());
char* start = mangle;
while (start != NULL) {
float xposAdj = (start == mangle) ? xpos : xpos + indentWidth;
char* brk = breakString(start,
(float) (mScreenWidth - mBorderWidth - xposAdj));
if (brk == NULL) {
// draw full string
drawString(texRender, Program::kIdentity, xposAdj, ypos,
String8(start));
start = NULL;
} else {
// draw partial string
char ch = *brk;
*brk = '\0';
drawString(texRender, Program::kIdentity, xposAdj, ypos,
String8(start));
*brk = ch;
start = brk;
if (strchr(kWhitespace, ch) != NULL) {
// if we broke on whitespace, skip past it
start++;
}
}
ypos += getScaledGlyphHeight();
}
free(mangle);
}
return ypos;
}
char* TextRenderer::breakString(const char* str, float maxWidth) const {
// Ideally we'd do clever things like binary search. Not bothering.
ALOGV("breakString '%s' %.3f", str, maxWidth);
size_t len = strlen(str);
if (len == 0) {
// Caller should detect this and not advance ypos.
return NULL;
}
float stringWidth = computeScaledStringWidth(str, len);
if (stringWidth <= maxWidth) {
return NULL; // trivial -- use full string
}
// Find the longest string that will fit.
size_t goodPos = 0;
for (size_t i = 0; i < len; i++) {
stringWidth = computeScaledStringWidth(str, i);
if (stringWidth < maxWidth) {
goodPos = i;
} else {
break; // too big
}
}
if (goodPos == 0) {
// space is too small to hold any glyph; output a single char
ALOGW("Couldn't find a nonzero prefix that fit from '%s'", str);
goodPos = 1;
}
// Scan back for whitespace. If we can't find any we'll just have
// an ugly mid-word break.
for (size_t i = goodPos; i > 0; i--) {
if (strchr(kWhitespace, str[i]) != NULL) {
goodPos = i;
break;
}
}
ALOGV("goodPos=%zu for str='%s'", goodPos, str);
return const_cast<char*>(str + goodPos);
}