move some utils into tldraw/utils (#1750)
This PR moves certain shared utilities (for images, etc.) to @tldraw/utils. ### Change Type - [x] `major` — Breaking change
This commit is contained in:
parent
f1f706f0b6
commit
6309cbe6a5
14 changed files with 283 additions and 249 deletions
|
@ -1067,15 +1067,6 @@ export const PI: number;
|
|||
// @public (undocumented)
|
||||
export const PI2: number;
|
||||
|
||||
// @public (undocumented)
|
||||
export const png: {
|
||||
isPng: typeof isPng;
|
||||
readChunks: typeof readChunks;
|
||||
parsePhys: typeof parsePhys;
|
||||
findChunk: typeof findChunk;
|
||||
setPhysChunk: typeof setPhysChunk;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function pointInBounds(A: VecLike, b: Box2d): boolean;
|
||||
|
||||
|
|
|
@ -289,7 +289,6 @@ export { getPointerInfo } from './lib/utils/getPointerInfo'
|
|||
export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints'
|
||||
export { hardResetEditor } from './lib/utils/hardResetEditor'
|
||||
export { normalizeWheel } from './lib/utils/normalizeWheel'
|
||||
export { png } from './lib/utils/png'
|
||||
export { refreshPage } from './lib/utils/refreshPage'
|
||||
export {
|
||||
getIndexAbove,
|
||||
|
|
|
@ -191,18 +191,6 @@ export function getFileMetaData(file: File): Promise<{
|
|||
isAnimated: boolean;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function getImageSizeFromSrc(dataURL: string): Promise<{
|
||||
w: number;
|
||||
h: number;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function getVideoSizeFromSrc(src: string): Promise<{
|
||||
w: number;
|
||||
h: number;
|
||||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
function Group({ children, size, }: {
|
||||
children: any;
|
||||
|
|
|
@ -110,13 +110,7 @@ export {
|
|||
} from './lib/ui/hooks/useTranslation/useTranslation'
|
||||
export { type TLUiIconType } from './lib/ui/icon-types'
|
||||
export { useDefaultHelpers, type TLUiOverrides } from './lib/ui/overrides'
|
||||
export {
|
||||
ACCEPTED_IMG_TYPE,
|
||||
getFileMetaData,
|
||||
getImageSizeFromSrc,
|
||||
getVideoSizeFromSrc,
|
||||
isImage,
|
||||
} from './lib/utils/assets'
|
||||
export { ACCEPTED_IMG_TYPE, getFileMetaData, isImage } from './lib/utils/assets'
|
||||
export { buildFromV1Document, type LegacyTldrawDocument } from './lib/utils/buildFromV1Document'
|
||||
export { getEmbedInfo } from './lib/utils/embeds'
|
||||
export {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
AssetRecordType,
|
||||
Editor,
|
||||
MediaHelpers,
|
||||
TLAsset,
|
||||
TLAssetId,
|
||||
TLEmbedShape,
|
||||
|
@ -21,9 +22,7 @@ import {
|
|||
ACCEPTED_VID_TYPE,
|
||||
containBoxSize,
|
||||
getFileMetaData,
|
||||
getImageSizeFromSrc,
|
||||
getResizedImageDataUrl,
|
||||
getVideoSizeFromSrc,
|
||||
isImage,
|
||||
} from './utils/assets'
|
||||
import { getEmbedInfo } from './utils/embeds'
|
||||
|
@ -46,7 +45,9 @@ export function useRegisterExternalContentHandlers() {
|
|||
let dataUrl = reader.result as string
|
||||
|
||||
const isImageType = isImage(file.type)
|
||||
const sizeFn = isImageType ? getImageSizeFromSrc : getVideoSizeFromSrc
|
||||
const sizeFn = isImageType
|
||||
? MediaHelpers.getImageSizeFromSrc
|
||||
: MediaHelpers.getVideoSizeFromSrc
|
||||
|
||||
// Hack to make .mov videos work via dataURL.
|
||||
if (file.type === 'video/quicktime' && dataUrl.includes('video/quicktime')) {
|
||||
|
|
|
@ -34,7 +34,7 @@ export function useDefaultEditorAssetsWithOverrides(
|
|||
if (!overrides) return defaultEditorAssetUrls
|
||||
|
||||
return {
|
||||
fonts: Object.assign({ ...defaultEditorAssetUrls.fonts }, { ...overrides?.fonts }),
|
||||
fonts: { ...defaultEditorAssetUrls.fonts, ...overrides?.fonts },
|
||||
}
|
||||
}, [overrides])
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { png } from '@tldraw/editor'
|
||||
import { isAnimated } from './is-gif-animated'
|
||||
|
||||
type BoxWidthHeight = {
|
||||
|
@ -94,18 +93,6 @@ export async function getResizedImageDataUrl(
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataURL - The file as a string.
|
||||
* @internal
|
||||
*
|
||||
* from https://stackoverflow.com/a/53817185
|
||||
*/
|
||||
async function base64ToFile(dataURL: string) {
|
||||
return fetch(dataURL).then(function (result) {
|
||||
return result.arrayBuffer()
|
||||
})
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const ACCEPTED_IMG_TYPE = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']
|
||||
/** @public */
|
||||
|
@ -113,62 +100,3 @@ export const ACCEPTED_VID_TYPE = ['video/mp4', 'video/quicktime']
|
|||
|
||||
/** @public */
|
||||
export const isImage = (ext: string) => ACCEPTED_IMG_TYPE.includes(ext)
|
||||
|
||||
/**
|
||||
* Get the size of a video from its source.
|
||||
*
|
||||
* @param src - The source of the video.
|
||||
* @public
|
||||
*/
|
||||
export async function getVideoSizeFromSrc(src: string): Promise<{ w: number; h: number }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video')
|
||||
video.onloadeddata = () => resolve({ w: video.videoWidth, h: video.videoHeight })
|
||||
video.onerror = (e) => {
|
||||
console.error(e)
|
||||
reject(new Error('Could not get video size'))
|
||||
}
|
||||
video.crossOrigin = 'anonymous'
|
||||
video.src = src
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of an image from its source.
|
||||
*
|
||||
* @param dataURL - The file as a string.
|
||||
* @public
|
||||
*/
|
||||
export async function getImageSizeFromSrc(dataURL: string): Promise<{ w: number; h: number }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = async () => {
|
||||
try {
|
||||
const blob = await base64ToFile(dataURL)
|
||||
const view = new DataView(blob)
|
||||
if (png.isPng(view, 0)) {
|
||||
const physChunk = png.findChunk(view, 'pHYs')
|
||||
if (physChunk) {
|
||||
const physData = png.parsePhys(view, physChunk.dataOffset)
|
||||
if (physData.unit === 0 && physData.ppux === physData.ppuy) {
|
||||
const pixelRatio = Math.round(physData.ppux / 2834.5)
|
||||
resolve({ w: img.width / pixelRatio, h: img.height / pixelRatio })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({ w: img.width, h: img.height })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
resolve({ w: img.width, h: img.height })
|
||||
}
|
||||
}
|
||||
img.onerror = (err) => {
|
||||
console.error(err)
|
||||
reject(new Error('Could not get image size'))
|
||||
}
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.src = dataURL
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { debugFlags, png } from '@tldraw/editor'
|
||||
import { PngHelpers, debugFlags } from '@tldraw/editor'
|
||||
import { getBrowserCanvasMaxSize } from '../shapes/shared/getBrowserCanvasMaxSize'
|
||||
|
||||
/** @public */
|
||||
|
@ -110,7 +110,7 @@ export async function getSvgAsImage(
|
|||
if (!blob) return null
|
||||
|
||||
const view = new DataView(await blob.arrayBuffer())
|
||||
return png.setPhysChunk(view, effectiveScale, {
|
||||
return PngHelpers.setPhysChunk(view, effectiveScale, {
|
||||
type: 'image/' + type,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
Editor,
|
||||
FileHelpers,
|
||||
MigrationFailureReason,
|
||||
MigrationResult,
|
||||
RecordId,
|
||||
|
@ -151,30 +152,6 @@ export function parseTldrawJsonFile({
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a file to base64.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* const A = fileToBase64('./test.png')
|
||||
* ```
|
||||
*
|
||||
* @param value - The file as a blob.
|
||||
* @public
|
||||
*/
|
||||
function fileToBase64(file: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.onerror = (error) => reject(error)
|
||||
reader.onabort = (error) => reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function serializeTldrawJson(store: TLStore): Promise<string> {
|
||||
const records: TLRecord[] = []
|
||||
|
@ -191,7 +168,9 @@ export async function serializeTldrawJson(store: TLStore): Promise<string> {
|
|||
let assetSrcToSave
|
||||
try {
|
||||
// try to save the asset as a base64 string
|
||||
assetSrcToSave = await fileToBase64(await (await fetch(record.props.src)).blob())
|
||||
assetSrcToSave = await FileHelpers.fileToBase64(
|
||||
await (await fetch(record.props.src)).blob()
|
||||
)
|
||||
} catch {
|
||||
// if that fails, just save the original src
|
||||
assetSrcToSave = record.props.src
|
||||
|
|
|
@ -42,6 +42,13 @@ export type Expand<T> = T extends infer O ? {
|
|||
[K in keyof O]: O[K];
|
||||
} : never;
|
||||
|
||||
// @public
|
||||
export class FileHelpers {
|
||||
// @internal (undocumented)
|
||||
static base64ToFile(dataURL: string): Promise<ArrayBuffer>;
|
||||
static fileToBase64(file: Blob): Promise<string>;
|
||||
}
|
||||
|
||||
// @internal
|
||||
export function filterEntries<Key extends string, Value>(object: {
|
||||
[K in Key]: Value;
|
||||
|
@ -112,6 +119,18 @@ export function mapObjectMapValues<Key extends string, ValueBefore, ValueAfter>(
|
|||
[K in Key]: ValueAfter;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class MediaHelpers {
|
||||
static getImageSizeFromSrc(dataURL: string): Promise<{
|
||||
w: number;
|
||||
h: number;
|
||||
}>;
|
||||
static getVideoSizeFromSrc(src: string): Promise<{
|
||||
w: number;
|
||||
h: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
export function minBy<T>(arr: readonly T[], fn: (item: T) => number): T | undefined;
|
||||
|
||||
|
@ -153,6 +172,34 @@ export function omitFromStackTrace<Args extends Array<unknown>, Return>(fn: (...
|
|||
// @internal
|
||||
export function partition<T>(arr: T[], predicate: (item: T) => boolean): [T[], T[]];
|
||||
|
||||
// @public (undocumented)
|
||||
export class PngHelpers {
|
||||
// (undocumented)
|
||||
static findChunk(view: DataView, type: string): {
|
||||
dataOffset: number;
|
||||
size: number;
|
||||
start: number;
|
||||
};
|
||||
// (undocumented)
|
||||
static getChunkType(view: DataView, offset: number): string;
|
||||
// (undocumented)
|
||||
static isPng(view: DataView, offset: number): boolean;
|
||||
// (undocumented)
|
||||
static parsePhys(view: DataView, offset: number): {
|
||||
ppux: number;
|
||||
ppuy: number;
|
||||
unit: number;
|
||||
};
|
||||
// (undocumented)
|
||||
static readChunks(view: DataView, offset?: number): Record<string, {
|
||||
dataOffset: number;
|
||||
size: number;
|
||||
start: number;
|
||||
}>;
|
||||
// (undocumented)
|
||||
static setPhysChunk(view: DataView, dpr?: number, options?: BlobPropertyBag): Blob;
|
||||
}
|
||||
|
||||
// @internal (undocumented)
|
||||
export function promiseWithResolve<T>(): Promise<T> & {
|
||||
resolve: (value: T) => void;
|
||||
|
|
|
@ -10,10 +10,12 @@ export {
|
|||
} from './lib/control'
|
||||
export { debounce } from './lib/debounce'
|
||||
export { annotateError, getErrorAnnotations } from './lib/error'
|
||||
export { FileHelpers } from './lib/file'
|
||||
export { noop, omitFromStackTrace, throttle } from './lib/function'
|
||||
export { getHashForObject, getHashForString, lns } from './lib/hash'
|
||||
export { getFirstFromIterable } from './lib/iterable'
|
||||
export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './lib/json-value'
|
||||
export { MediaHelpers } from './lib/media'
|
||||
export { lerp, modulate, rng } from './lib/number'
|
||||
export {
|
||||
deepCopy,
|
||||
|
@ -26,6 +28,7 @@ export {
|
|||
objectMapKeys,
|
||||
objectMapValues,
|
||||
} from './lib/object'
|
||||
export { PngHelpers } from './lib/png'
|
||||
export { rafThrottle, throttledRaf } from './lib/raf'
|
||||
export { sortById } from './lib/sort'
|
||||
export type { Expand, RecursivePartial, Required } from './lib/types'
|
||||
|
|
42
packages/utils/src/lib/file.ts
Normal file
42
packages/utils/src/lib/file.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Helpers for files
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class FileHelpers {
|
||||
/**
|
||||
* @param dataURL - The file as a string.
|
||||
* @internal
|
||||
*
|
||||
* from https://stackoverflow.com/a/53817185
|
||||
*/
|
||||
static async base64ToFile(dataURL: string) {
|
||||
return fetch(dataURL).then(function (result) {
|
||||
return result.arrayBuffer()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a file to base64.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* const A = fileToBase64('./test.png')
|
||||
* ```
|
||||
*
|
||||
* @param value - The file as a blob.
|
||||
* @public
|
||||
*/
|
||||
static async fileToBase64(file: Blob): Promise<string> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.onerror = (error) => reject(error)
|
||||
reader.onabort = (error) => reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
68
packages/utils/src/lib/media.ts
Normal file
68
packages/utils/src/lib/media.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { FileHelpers } from './file'
|
||||
import { PngHelpers } from './png'
|
||||
|
||||
/**
|
||||
* Helpers for media
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class MediaHelpers {
|
||||
/**
|
||||
* Get the size of a video from its source.
|
||||
*
|
||||
* @param src - The source of the video.
|
||||
* @public
|
||||
*/
|
||||
static async getVideoSizeFromSrc(src: string): Promise<{ w: number; h: number }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video')
|
||||
video.onloadeddata = () => resolve({ w: video.videoWidth, h: video.videoHeight })
|
||||
video.onerror = (e) => {
|
||||
console.error(e)
|
||||
reject(new Error('Could not get video size'))
|
||||
}
|
||||
video.crossOrigin = 'anonymous'
|
||||
video.src = src
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of an image from its source.
|
||||
*
|
||||
* @param dataURL - The file as a string.
|
||||
* @public
|
||||
*/
|
||||
static async getImageSizeFromSrc(dataURL: string): Promise<{ w: number; h: number }> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = async () => {
|
||||
try {
|
||||
const blob = await FileHelpers.base64ToFile(dataURL)
|
||||
const view = new DataView(blob)
|
||||
if (PngHelpers.isPng(view, 0)) {
|
||||
const physChunk = PngHelpers.findChunk(view, 'pHYs')
|
||||
if (physChunk) {
|
||||
const physData = PngHelpers.parsePhys(view, physChunk.dataOffset)
|
||||
if (physData.unit === 0 && physData.ppux === physData.ppuy) {
|
||||
const pixelRatio = Math.round(physData.ppux / 2834.5)
|
||||
resolve({ w: img.width / pixelRatio, h: img.height / pixelRatio })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({ w: img.width, h: img.height })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
resolve({ w: img.width, h: img.height })
|
||||
}
|
||||
}
|
||||
img.onerror = (err) => {
|
||||
console.error(err)
|
||||
reject(new Error('Could not get image size'))
|
||||
}
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.src = dataURL
|
||||
})
|
||||
}
|
||||
}
|
|
@ -54,125 +54,119 @@ const crc: CRCCalculator<Uint8Array> = (current, previous) => {
|
|||
return crc ^ -1
|
||||
}
|
||||
|
||||
function isPng(view: DataView, offset: number) {
|
||||
if (
|
||||
view.getUint8(offset + 0) === 0x89 &&
|
||||
view.getUint8(offset + 1) === 0x50 &&
|
||||
view.getUint8(offset + 2) === 0x4e &&
|
||||
view.getUint8(offset + 3) === 0x47 &&
|
||||
view.getUint8(offset + 4) === 0x0d &&
|
||||
view.getUint8(offset + 5) === 0x0a &&
|
||||
view.getUint8(offset + 6) === 0x1a &&
|
||||
view.getUint8(offset + 7) === 0x0a
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getChunkType(view: DataView, offset: number) {
|
||||
return [
|
||||
String.fromCharCode(view.getUint8(offset)),
|
||||
String.fromCharCode(view.getUint8(offset + 1)),
|
||||
String.fromCharCode(view.getUint8(offset + 2)),
|
||||
String.fromCharCode(view.getUint8(offset + 3)),
|
||||
].join('')
|
||||
}
|
||||
|
||||
const LEN_SIZE = 4
|
||||
const CRC_SIZE = 4
|
||||
|
||||
function readChunks(view: DataView, offset = 0) {
|
||||
const chunks: Record<string, { dataOffset: number; size: number; start: number }> = {}
|
||||
if (!isPng(view, offset)) {
|
||||
throw new Error('Not a PNG')
|
||||
}
|
||||
offset += 8
|
||||
|
||||
while (offset <= view.buffer.byteLength) {
|
||||
const start = offset
|
||||
const len = view.getInt32(offset)
|
||||
offset += 4
|
||||
const chunkType = getChunkType(view, offset)
|
||||
|
||||
if (chunkType === 'IDAT' && chunks[chunkType]) {
|
||||
offset += len + LEN_SIZE + CRC_SIZE
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunkType === 'IEND') {
|
||||
break
|
||||
}
|
||||
|
||||
chunks[chunkType] = {
|
||||
start,
|
||||
dataOffset: offset + 4,
|
||||
size: len,
|
||||
}
|
||||
offset += len + LEN_SIZE + CRC_SIZE
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
function parsePhys(view: DataView, offset: number) {
|
||||
return {
|
||||
ppux: view.getUint32(offset),
|
||||
ppuy: view.getUint32(offset + 4),
|
||||
unit: view.getUint8(offset + 4),
|
||||
}
|
||||
}
|
||||
|
||||
function findChunk(view: DataView, type: string) {
|
||||
const chunks = readChunks(view)
|
||||
return chunks[type]
|
||||
}
|
||||
|
||||
function setPhysChunk(view: DataView, dpr = 1, options?: BlobPropertyBag) {
|
||||
let offset = 46
|
||||
let size = 0
|
||||
const res1 = findChunk(view, 'pHYs')
|
||||
if (res1) {
|
||||
offset = res1.start
|
||||
size = res1.size
|
||||
}
|
||||
|
||||
const res2 = findChunk(view, 'IDAT')
|
||||
if (res2) {
|
||||
offset = res2.start
|
||||
size = 0
|
||||
}
|
||||
|
||||
const pHYsData = new ArrayBuffer(21)
|
||||
const pHYsDataView = new DataView(pHYsData)
|
||||
|
||||
pHYsDataView.setUint32(0, 9)
|
||||
|
||||
pHYsDataView.setUint8(4, 'p'.charCodeAt(0))
|
||||
pHYsDataView.setUint8(5, 'H'.charCodeAt(0))
|
||||
pHYsDataView.setUint8(6, 'Y'.charCodeAt(0))
|
||||
pHYsDataView.setUint8(7, 's'.charCodeAt(0))
|
||||
|
||||
const DPI_96 = 2835.5
|
||||
|
||||
pHYsDataView.setInt32(8, DPI_96 * dpr)
|
||||
pHYsDataView.setInt32(12, DPI_96 * dpr)
|
||||
pHYsDataView.setInt8(16, 1)
|
||||
|
||||
const crcBit = new Uint8Array(pHYsData.slice(4, 17))
|
||||
pHYsDataView.setInt32(17, crc(crcBit))
|
||||
|
||||
const startBuf = view.buffer.slice(0, offset)
|
||||
const endBuf = view.buffer.slice(offset + size)
|
||||
|
||||
return new Blob([startBuf, pHYsData, endBuf], options)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const png = {
|
||||
isPng,
|
||||
readChunks,
|
||||
parsePhys,
|
||||
findChunk,
|
||||
setPhysChunk,
|
||||
export class PngHelpers {
|
||||
static isPng(view: DataView, offset: number) {
|
||||
if (
|
||||
view.getUint8(offset + 0) === 0x89 &&
|
||||
view.getUint8(offset + 1) === 0x50 &&
|
||||
view.getUint8(offset + 2) === 0x4e &&
|
||||
view.getUint8(offset + 3) === 0x47 &&
|
||||
view.getUint8(offset + 4) === 0x0d &&
|
||||
view.getUint8(offset + 5) === 0x0a &&
|
||||
view.getUint8(offset + 6) === 0x1a &&
|
||||
view.getUint8(offset + 7) === 0x0a
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static getChunkType(view: DataView, offset: number) {
|
||||
return [
|
||||
String.fromCharCode(view.getUint8(offset)),
|
||||
String.fromCharCode(view.getUint8(offset + 1)),
|
||||
String.fromCharCode(view.getUint8(offset + 2)),
|
||||
String.fromCharCode(view.getUint8(offset + 3)),
|
||||
].join('')
|
||||
}
|
||||
|
||||
static readChunks(view: DataView, offset = 0) {
|
||||
const chunks: Record<string, { dataOffset: number; size: number; start: number }> = {}
|
||||
if (!PngHelpers.isPng(view, offset)) {
|
||||
throw new Error('Not a PNG')
|
||||
}
|
||||
offset += 8
|
||||
|
||||
while (offset <= view.buffer.byteLength) {
|
||||
const start = offset
|
||||
const len = view.getInt32(offset)
|
||||
offset += 4
|
||||
const chunkType = PngHelpers.getChunkType(view, offset)
|
||||
|
||||
if (chunkType === 'IDAT' && chunks[chunkType]) {
|
||||
offset += len + LEN_SIZE + CRC_SIZE
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunkType === 'IEND') {
|
||||
break
|
||||
}
|
||||
|
||||
chunks[chunkType] = {
|
||||
start,
|
||||
dataOffset: offset + 4,
|
||||
size: len,
|
||||
}
|
||||
offset += len + LEN_SIZE + CRC_SIZE
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
static parsePhys(view: DataView, offset: number) {
|
||||
return {
|
||||
ppux: view.getUint32(offset),
|
||||
ppuy: view.getUint32(offset + 4),
|
||||
unit: view.getUint8(offset + 4),
|
||||
}
|
||||
}
|
||||
|
||||
static findChunk(view: DataView, type: string) {
|
||||
const chunks = PngHelpers.readChunks(view)
|
||||
return chunks[type]
|
||||
}
|
||||
|
||||
static setPhysChunk(view: DataView, dpr = 1, options?: BlobPropertyBag) {
|
||||
let offset = 46
|
||||
let size = 0
|
||||
const res1 = PngHelpers.findChunk(view, 'pHYs')
|
||||
if (res1) {
|
||||
offset = res1.start
|
||||
size = res1.size
|
||||
}
|
||||
|
||||
const res2 = PngHelpers.findChunk(view, 'IDAT')
|
||||
if (res2) {
|
||||
offset = res2.start
|
||||
size = 0
|
||||
}
|
||||
|
||||
const pHYsData = new ArrayBuffer(21)
|
||||
const pHYsDataView = new DataView(pHYsData)
|
||||
|
||||
pHYsDataView.setUint32(0, 9)
|
||||
|
||||
pHYsDataView.setUint8(4, 'p'.charCodeAt(0))
|
||||
pHYsDataView.setUint8(5, 'H'.charCodeAt(0))
|
||||
pHYsDataView.setUint8(6, 'Y'.charCodeAt(0))
|
||||
pHYsDataView.setUint8(7, 's'.charCodeAt(0))
|
||||
|
||||
const DPI_96 = 2835.5
|
||||
|
||||
pHYsDataView.setInt32(8, DPI_96 * dpr)
|
||||
pHYsDataView.setInt32(12, DPI_96 * dpr)
|
||||
pHYsDataView.setInt8(16, 1)
|
||||
|
||||
const crcBit = new Uint8Array(pHYsData.slice(4, 17))
|
||||
pHYsDataView.setInt32(17, crc(crcBit))
|
||||
|
||||
const startBuf = view.buffer.slice(0, offset)
|
||||
const endBuf = view.buffer.slice(offset + size)
|
||||
|
||||
return new Blob([startBuf, pHYsData, endBuf], options)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue