Shared glyph rendering logic for normal and rotated text. Coordinate mapping and cursor advance direction are selected at compile time via the template parameter. Render a glyph at 50% scale. Used for SUP/SUB style bits. Each destination pixel represents a 2x2 source block. Drawing when that block contains ink preserves thin strokes that nearest-neighbor sampling can skip. The advance width is a
| 146 | // The advance width is also halved in drawText() so layout reserves exactly the right |
| 147 | // horizontal space for the scaled glyph. |
| 148 | static void renderCharScaled(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode, |
| 149 | const EpdFontFamily& fontFamily, const uint32_t cp, int cursorX, int cursorY, |
| 150 | const bool pixelState, const EpdFontFamily::Style style) { |
| 151 | const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); |
| 152 | if (!glyph) return; |
| 153 | |
| 154 | const EpdFontData* fontData = fontFamily.getData(style); |
| 155 | const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph); |
| 156 | if (!bitmap) return; |
| 157 | |
| 158 | const int srcW = glyph->width; |
| 159 | const int srcH = glyph->height; |
| 160 | const int dstW = (srcW + 1) / 2; // ceil so odd-width glyphs aren't clipped |
| 161 | const int dstH = (srcH + 1) / 2; |
| 162 | // Scale the glyph bearing by the same factor so the scaled glyph sits at the correct |
| 163 | // pixel offset from the (already-shifted) cursor position. |
| 164 | const int baseX = cursorX + glyph->left / 2; |
| 165 | const int baseY = cursorY - glyph->top / 2; |
| 166 | |
| 167 | if (fontData->is2Bit) { |
| 168 | // 2-bit packed format: 4 pixels per byte, MSB first, 2 bits per pixel. |
| 169 | // raw value: 0=white, 1=light-gray, 2=dark-gray, 3=black. |
| 170 | for (int dstY = 0; dstY < dstH; dstY++) { |
| 171 | const int srcY = dstY * 2; |
| 172 | for (int dstX = 0; dstX < dstW; dstX++) { |
| 173 | const int srcX = dstX * 2; |
| 174 | uint8_t coverage = 0; |
| 175 | uint8_t maxRaw = 0; |
| 176 | for (int sampleY = 0; sampleY < 2 && srcY + sampleY < srcH; sampleY++) { |
| 177 | for (int sampleX = 0; sampleX < 2 && srcX + sampleX < srcW; sampleX++) { |
| 178 | const int pos = (srcY + sampleY) * srcW + srcX + sampleX; |
| 179 | const uint8_t byte = bitmap[pos >> 2]; |
| 180 | const uint8_t raw = (byte >> ((3 - (pos & 3)) * 2)) & 0x3; |
| 181 | coverage += raw; |
| 182 | if (raw > maxRaw) maxRaw = raw; |
| 183 | } |
| 184 | } |
| 185 | if (maxRaw >= 2 || coverage >= 2) { |
| 186 | renderer.drawPixel(baseX + dstX, baseY + dstY, pixelState); |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | } else { |
| 191 | // 1-bit packed format: 8 pixels per byte, MSB first. |
| 192 | for (int dstY = 0; dstY < dstH; dstY++) { |
| 193 | const int srcY = dstY * 2; |
| 194 | for (int dstX = 0; dstX < dstW; dstX++) { |
| 195 | const int srcX = dstX * 2; |
| 196 | bool hasInk = false; |
| 197 | for (int sampleY = 0; sampleY < 2 && srcY + sampleY < srcH; sampleY++) { |
| 198 | for (int sampleX = 0; sampleX < 2 && srcX + sampleX < srcW; sampleX++) { |
| 199 | const int pos = (srcY + sampleY) * srcW + srcX + sampleX; |
| 200 | const uint8_t byte = bitmap[pos >> 3]; |
| 201 | const uint8_t bit = 7 - (pos & 7); |
| 202 | if ((byte >> bit) & 1) { |
| 203 | hasInk = true; |
| 204 | } |
| 205 | } |
no test coverage detected