Skip to content

Commit

Permalink
fix: rendering avatar (#10)
Browse files Browse the repository at this point in the history
* fix: rendering avatar

* fix: block size
  • Loading branch information
LuciNyan authored Feb 28, 2024
1 parent a4794a0 commit 0d86984
Show file tree
Hide file tree
Showing 16 changed files with 115 additions and 32 deletions.
28 changes: 9 additions & 19 deletions packages/pixel-profile/src/cards/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
} from '../templates/github-stats'
import { getThemeOptions } from '../theme'
import { getBase64FromPixels, getPixelsFromPngBuffer, getPngBufferFromPixels, kFormatter, Rank } from '../utils'
import { getPngBufferFromURL } from '../utils/converter'
import { filterNotEmpty } from '../utils/filter'
import { Resvg } from '@resvg/resvg-js'
import axios from 'axios'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import satori from 'satori'
Expand Down Expand Up @@ -58,10 +58,7 @@ export async function renderStats(stats: Stats, options: Options = {}): Promise<

const fontPath = join(process.cwd(), 'packages', 'pixel-profile', 'fonts', 'PressStart2P-Regular.ttf')

const [fontData, avatar] = await Promise.all([
readFile(fontPath),
makeAvatar(avatarUrl, pixelateAvatar, AVATAR_SIZE.AVATAR_WIDTH, AVATAR_SIZE.AVATAR_HEIGHT)
])
const [fontData, avatar] = await Promise.all([readFile(fontPath), makeAvatar(avatarUrl, pixelateAvatar)])

const _stats = {
name,
Expand Down Expand Up @@ -134,7 +131,7 @@ export async function renderStats(stats: Stats, options: Options = {}): Promise<
const pngData = new Resvg(svg, opts).render()
const pngBuffer = pngData.asPng()

let pixels = await getPixelsFromPngBuffer(pngBuffer)
let { pixels } = await getPixelsFromPngBuffer(pngBuffer)

if (screenEffect) {
pixels = curve(pixels, width, height)
Expand All @@ -143,26 +140,19 @@ export async function renderStats(stats: Stats, options: Options = {}): Promise<
return await getPngBufferFromPixels(pixels, width, height)
}

async function makeAvatar(
url: string,
pixelateAvatar: boolean,
width: number,
height: number,
blockSize: number = 6.8
): Promise<string> {
const BLOCK_SIZE = 6.8

async function makeAvatar(url: string, pixelateAvatar: boolean): Promise<string> {
if (!url) {
return ''
}

const response = await axios.get(url, {
responseType: 'arraybuffer'
})

const png = Buffer.from(response.data, 'binary')
const png: Buffer = await getPngBufferFromURL(url)

let pixels = await getPixelsFromPngBuffer(png)
let { pixels, width, height } = await getPixelsFromPngBuffer(png)

if (pixelateAvatar) {
const blockSize = (height / AVATAR_SIZE.AVATAR_HEIGHT) * BLOCK_SIZE
pixels = pixelate(pixels, width, height, blockSize)
}

Expand Down
15 changes: 15 additions & 0 deletions packages/pixel-profile/src/utils/compare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function compare(str1: string | Array<number> | Buffer, str2: string | Array<number> | Buffer): number {
const minLength = Math.min(str1.length, str2.length)

for (let i = 0; i < minLength; i++) {
if (str1[i] !== str2[i]) {
return i
}
}

if (str1.length !== str2.length) {
return minLength
}

return -1
}
50 changes: 38 additions & 12 deletions packages/pixel-profile/src/utils/converter.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import { isBase64PNG } from './is'
import axios from 'axios'
import Jimp from 'jimp'

export async function getPixelsFromPngBuffer(pngBuffer: Buffer): Promise<Buffer> {
const image = await Jimp.read(pngBuffer)
export async function getPixelsFromPngBuffer(png: Buffer): Promise<{
pixels: Buffer
width: number
height: number
}> {
const image = await Jimp.read(png)

const width = image.getWidth()
const height = image.getHeight()
const pixelsBuffer = Buffer.alloc(width * height * 4)
const pixels = Buffer.alloc(width * height * 4)

image.scan(0, 0, width, height, (x, y, idx) => {
pixelsBuffer[idx] = image.bitmap.data[idx]
pixelsBuffer[idx + 1] = image.bitmap.data[idx + 1]
pixelsBuffer[idx + 2] = image.bitmap.data[idx + 2]
pixelsBuffer[idx + 3] = image.bitmap.data[idx + 3]
pixels[idx] = image.bitmap.data[idx]
pixels[idx + 1] = image.bitmap.data[idx + 1]
pixels[idx + 2] = image.bitmap.data[idx + 2]
pixels[idx + 3] = image.bitmap.data[idx + 3]
})

return pixelsBuffer
return {
pixels,
width,
height
}
}

export function getBase64FromPixels(pixelsBuffer: Buffer, width: number, height: number): Promise<string> {
export async function getBase64FromPixels(pixels: Buffer, width: number, height: number): Promise<string> {
return new Promise((resolve) => {
// eslint-disable-next-line no-new
new Jimp(width, height, function (_, image) {
image.bitmap.data = pixelsBuffer
image.getBase64('image/png', function (_, str) {
new Jimp(width, height, (_, image) => {
image.bitmap.data = pixels
image.getBase64('image/png', (_, str) => {
resolve(str)
})
})
Expand All @@ -40,3 +50,19 @@ export function getPngBufferFromPixels(pixelsBuffer: Buffer, width: number, heig
})
})
}

export function getPngBufferFromBase64(base64: string): Buffer {
return Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64')
}

export async function getPngBufferFromURL(url: string): Promise<Buffer> {
if (isBase64PNG(url)) {
return getPngBufferFromBase64(url)
} else {
return await new Promise((resolve) => {
axios.get(url, { responseType: 'arraybuffer' }).then((response) => {
resolve(Buffer.from(response.data, 'binary'))
})
})
}
}
1 change: 1 addition & 0 deletions packages/pixel-profile/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { compare } from './compare'
export { getBase64FromPixels, getPixelsFromPngBuffer, getPngBufferFromPixels } from './converter'
export { add2, clamp, dot2, kFormatter, prod2, subtract2, type Vec2 } from './math'
export { type Rank, rank } from './rank'
Expand Down
3 changes: 3 additions & 0 deletions packages/pixel-profile/src/utils/is.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isBase64PNG(src: string): boolean {
return src.startsWith('data:image/png;base64,')
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions packages/pixel-profile/test/github-stats.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { renderStats } from '../src'
import { BLUE_BASE64_PNG } from './img/blue'
// @ts-expect-error ...
import { toMatchImageSnapshot } from 'jest-image-snapshot'
import { describe, expect, it } from 'vitest'
Expand All @@ -15,6 +16,49 @@ declare global {
expect.extend({ toMatchImageSnapshot })

describe('Github stats', () => {
it('Render card', async () => {
const png = await renderStats({
name: 'LuciNyan',
username: 'username',
totalStars: 999,
totalCommits: 99999,
totalIssues: 99,
totalPRs: 9,
contributedTo: 9999,
avatarUrl: BLUE_BASE64_PNG,
rank: {
level: 'S',
percentile: 0,
score: 0
}
})
expect(png).toMatchImageSnapshot()
})

it('Render card without pixelate avatar', async () => {
const png = await renderStats(
{
name: 'LuciNyan',
username: 'username',
totalStars: 999,
totalCommits: 99999,
totalIssues: 99,
totalPRs: 9,
contributedTo: 9999,
avatarUrl: BLUE_BASE64_PNG,
rank: {
level: 'S',
percentile: 0,
score: 0
}
},
{
pixelateAvatar: false
}
)
expect(png).toMatchImageSnapshot()
})

it('Render card without avatar', async () => {
const png = await renderStats({
name: 'LuciNyan',
Expand Down
3 changes: 3 additions & 0 deletions packages/pixel-profile/test/img/blue.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/pixel-profile/test/theme.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { renderStats } from '../src'
import { BLUE_BASE64_PNG } from './img/blue'
// @ts-expect-error ...
import { toMatchImageSnapshot } from 'jest-image-snapshot'
import { describe, expect, it } from 'vitest'
Expand All @@ -22,7 +23,7 @@ const stats = {
totalIssues: 99,
totalPRs: 9,
contributedTo: 9999,
avatarUrl: '',
avatarUrl: BLUE_BASE64_PNG,
rank: {
level: 'S',
percentile: 0,
Expand Down

0 comments on commit 0d86984

Please sign in to comment.