/* * 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 #include "TextRenderer.h" #include 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=%d for str='%s'", goodPos, str); return const_cast(str + goodPos); }