361 lines
12 KiB
C++
361 lines
12 KiB
C++
/*
|
|
* 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);
|
|
}
|