-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Downscale pasted PNG images based on metadata (#29123)
Some images like MacOS screenshots contain [pHYs](http://www.libpng.org/pub/png/book/chapter11.html#png.ch11.div.8) data which we can use to downscale uploaded images so they render in the same dppx ratio in which they were taken. Before: <img width="584" alt="image" src="https://github.com/go-gitea/gitea/assets/115237/50979e3a-5d5a-40dc-a0a4-36eb6e28f14a"> After: <img width="329" alt="image" src="https://github.com/go-gitea/gitea/assets/115237/0690902a-f2fe-4c6b-97b3-6fdd67c21bad">
- Loading branch information
1 parent
f04e71f
commit 5e72526
Showing
3 changed files
with
93 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
export async function pngChunks(blob) { | ||
const uint8arr = new Uint8Array(await blob.arrayBuffer()); | ||
const chunks = []; | ||
if (uint8arr.length < 12) return chunks; | ||
const view = new DataView(uint8arr.buffer); | ||
if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; | ||
|
||
const decoder = new TextDecoder(); | ||
let index = 8; | ||
while (index < uint8arr.length) { | ||
const len = view.getUint32(index); | ||
chunks.push({ | ||
name: decoder.decode(uint8arr.slice(index + 4, index + 8)), | ||
data: uint8arr.slice(index + 8, index + 8 + len), | ||
}); | ||
index += len + 12; | ||
} | ||
|
||
return chunks; | ||
} | ||
|
||
// decode a image and try to obtain width and dppx. If will never throw but instead | ||
// return default values. | ||
export async function imageInfo(blob) { | ||
let width = 0; // 0 means no width could be determined | ||
let dppx = 1; // 1 dot per pixel for non-HiDPI screens | ||
|
||
if (blob.type === 'image/png') { // only png is supported currently | ||
try { | ||
for (const {name, data} of await pngChunks(blob)) { | ||
const view = new DataView(data.buffer); | ||
if (name === 'IHDR' && data?.length) { | ||
// extract width from mandatory IHDR chunk | ||
width = view.getUint32(0); | ||
} else if (name === 'pHYs' && data?.length) { | ||
// extract dppx from optional pHYs chunk, assuming pixels are square | ||
const unit = view.getUint8(8); | ||
if (unit === 1) { | ||
dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx | ||
} | ||
} | ||
} | ||
} catch {} | ||
} | ||
|
||
return {width, dppx}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import {pngChunks, imageInfo} from './image.js'; | ||
|
||
const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg=='; | ||
const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; | ||
const pngEmpty = 'data:image/png;base64,'; | ||
|
||
async function dataUriToBlob(datauri) { | ||
return await (await globalThis.fetch(datauri)).blob(); | ||
} | ||
|
||
test('pngChunks', async () => { | ||
expect(await pngChunks(await dataUriToBlob(pngNoPhys))).toEqual([ | ||
{name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])}, | ||
{name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])}, | ||
{name: 'IEND', data: new Uint8Array([])}, | ||
]); | ||
expect(await pngChunks(await dataUriToBlob(pngPhys))).toEqual([ | ||
{name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])}, | ||
{name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])}, | ||
{name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])}, | ||
]); | ||
expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]); | ||
}); | ||
|
||
test('imageInfo', async () => { | ||
expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); | ||
expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2}); | ||
expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1}); | ||
}); |