| /* | |
| * 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); | |
| } |