Skip to content

Commit

Permalink
feat: support more source type
Browse files Browse the repository at this point in the history
  • Loading branch information
qq15725 committed May 10, 2023
1 parent a015438 commit 8dbabb2
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 104 deletions.
48 changes: 23 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,47 +29,47 @@ npm i modern-gif
### Encode

```ts
import { createEncoder } from 'modern-gif'
import { encode } from 'modern-gif'
// import the `workerUrl` through `Vite`
import workerUrl from 'modern-gif/worker?url'

const width = 100
const height = 100

const encoder = createEncoder({
const output = await encode({
workerUrl,
width,
height,
})

encoder.encode({
imageData: new Uint8ClampedArray(width * height * 4).map(() => 111),
delay: 100,
workerNumber: 2,
width: 200,
height: 200,
frames: [
{
imageData: '/example1.png',
delay: 100,
},
{
imageData: '/example2.png',
delay: 100,
}
]
})

encoder.encode({
imageData: new Uint8ClampedArray(width * height * 4).map(() => 222),
delay: 100,
})

encoder.flush().then(data => {
const blob = new Blob([data], { type: 'image/gif' })
window.open(URL.createObjectURL(blob))
})
const blob = new Blob([output], { type: 'image/gif' })
window.open(URL.createObjectURL(blob))
```

### Decode

```ts
import { decode, decodeFrames } from 'modern-gif'
import { decode, decodeFramesInWorker } from 'modern-gif'
// import the `workerUrl` through `Vite`
import workerUrl from 'modern-gif/worker?url'

window.fetch('https://raw.githubusercontent.com/qq15725/modern-gif/master/test/assets/test.gif')
.then(res => res.arrayBuffer())
.then(buffer => new Uint8Array(buffer))
.then(data => {
const gif = decode(data)

decodeFrames(data, gif).forEach(frame => {
console.log(gif)

decodeFramesInWorker(data, workerUrl).forEach(frame => {
const canvas = document.createElement('canvas')
const context2d = canvas.getContext('2d')
canvas.width = frame.width
Expand All @@ -80,8 +80,6 @@ window.fetch('https://raw.githubusercontent.com/qq15725/modern-gif/master/test/a
)
document.body.append(canvas)
})

console.log(gif)
})
```

Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ <h2>Demo</h2>
<div>
<p>
<label>MaxColors</label>
<input type="range" step="1" min="2" max="256" value="10" data-name="maxColors">
<input type="range" step="1" min="2" max="255" value="10" data-name="maxColors">
<span class="value">10</span>
</p>
<p>
Expand Down
68 changes: 44 additions & 24 deletions src/create-encoder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createPalette } from 'modern-palette'
import { TRAILER, mergeUint8Array } from './utils'
import { TRAILER, cropBuffer, loadImage, mergeBuffers, resovleSource, resovleSourceBox } from './utils'
import { encodeHeader } from './encode-header'
import { encodeFrame } from './encode-frame'
import { indexFrames } from './index-frames'
Expand All @@ -26,7 +26,7 @@ export function createEncoder(options: EncoderOptions) {
maxColors = colorTableSize - 1,
} = options

let frames: EncodeFrameOptions[] = []
let frames: EncodeFrameOptions<Uint8ClampedArray>[] = []

const transparentIndex = backgroundColorIndex
const log = createLogger(debug)
Expand All @@ -37,12 +37,10 @@ export function createEncoder(options: EncoderOptions) {
workerNumber,
})

async function addSampleInWorker(
options: Uint8ClampedArray,
): Promise<void> {
async function addSampleInWorker(options: Uint8ClampedArray): Promise<void> {
const result = await worker.call(
{ type: 'palette:addSample', options },
[options.buffer],
undefined,
0,
)
if (result) return
Expand All @@ -60,20 +58,15 @@ export function createEncoder(options: EncoderOptions) {
return palette.context
}

async function indexFramesInWorker(
options: IndexFramesOptions,
): Promise<ReturnType<typeof indexFrames>> {
async function indexFramesInWorker(options: IndexFramesOptions): Promise<ReturnType<typeof indexFrames>> {
const result = await worker.call(
{ type: 'frames:index', options },
options.frames.map(val => val.imageData.buffer),
)
if (result) return result as any
return indexFrames(options)
}

async function cropFramesInWorker(
options: CropFramesOptions,
): Promise<ReturnType<typeof cropFrames>> {
async function cropFramesInWorker(options: CropFramesOptions): Promise<ReturnType<typeof cropFrames>> {
const result = await worker.call(
{ type: 'frames:crop', options },
options.frames.map(val => val.imageData.buffer),
Expand All @@ -82,9 +75,7 @@ export function createEncoder(options: EncoderOptions) {
return cropFrames(options)
}

async function encodeFrameInWorker(
options: EncodeFrameOptions,
): Promise<ReturnType<typeof encodeFrame>> {
async function encodeFrameInWorker(options: EncodeFrameOptions<Uint8ClampedArray>): Promise<ReturnType<typeof encodeFrame>> {
const result = await worker.call(
{ type: 'frame:encode', options },
[options.imageData.buffer],
Expand All @@ -97,14 +88,43 @@ export function createEncoder(options: EncoderOptions) {
setMaxColors(value: number): void {
maxColors = value
},
async encode(frame: EncodeFrameOptions): Promise<void> {
async encode(options: EncodeFrameOptions): Promise<void> {
const index = frames.length
if (index === 0) {
await worker.call({ type: 'palette:init' }, undefined, 0)
}
if (index === 0) await worker.call({ type: 'palette:init' }, undefined, 0)

const { width: frameWidth = width, height: frameHeight = height } = options
let { imageData: source } = options

log.time(`palette:sample-${ index }`)
frames.push({ width, height, ...frame })
await addSampleInWorker(frame.imageData.slice(0))

source = typeof source === 'string'
? await loadImage(source)
: source

const box = resovleSourceBox(source)

let imageData = resovleSource(source, 'uint8ClampedArray')

if (box && frameWidth && frameHeight) {
imageData = cropBuffer(
resovleSource(source, 'uint8ClampedArray'),
{
width: frameWidth,
height: frameHeight,
rawWidth: box.width,
},
)
}

frames.push({
width: frameWidth,
height: frameHeight,
...options,
imageData,
})

await addSampleInWorker(imageData)

log.timeEnd(`palette:sample-${ index }`)
},
async flush(): Promise<Uint8Array> {
Expand All @@ -120,7 +140,7 @@ export function createEncoder(options: EncoderOptions) {

log.time('frames:index')
const indexedFrames = await indexFramesInWorker({
frames: frames.map(frame => ({ imageData: frame.imageData.slice(0) })),
frames,
palette: context,
transparentIndex,
})
Expand Down Expand Up @@ -162,7 +182,7 @@ export function createEncoder(options: EncoderOptions) {
backgroundColorIndex,
...options,
})
const body = mergeUint8Array(...encodedFrames)
const body = mergeBuffers(encodedFrames)
const output = new Uint8Array(header.length + body.byteLength + 1)
output.set(header)
output.set(body, header.byteLength)
Expand Down
4 changes: 2 additions & 2 deletions src/create-reader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resovleDataView } from './utils'
import { resovleSource } from './utils'

export function createReader(data: BufferSource) {
const view = resovleDataView(data)
const view = resovleSource(data, 'dataView')

let cursor = 0

Expand Down
32 changes: 20 additions & 12 deletions src/crop-frames.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { cropBuffer } from './utils'

export interface CropFramesOptions {
frames: {
width: number
Expand Down Expand Up @@ -145,18 +147,24 @@ export function cropFrames(options: CropFramesOptions) {

const newWidth = right + 1 - left
const newHeight = bottom + 1 - top
const croppedIndexPixels = new Uint8ClampedArray(newWidth * newHeight)
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
const index = y * newWidth + x
const rawIndex = (top + y) * width + (left + x)
if (!transparent && prevIndexPixels && indexPixels[rawIndex] === prevIndexPixels[rawIndex]) {
croppedIndexPixels[index] = transparentIndex
continue
}
croppedIndexPixels[index] = indexPixels[rawIndex]
}
}

const croppedIndexPixels = cropBuffer(
indexPixels,
{
left,
top,
width: newWidth,
height: newHeight,
rawWidth: width,
rate: 1,
callback: rawIndex => {
if (!transparent && prevIndexPixels && indexPixels[rawIndex] === prevIndexPixels[rawIndex]) {
return transparentIndex
}
return undefined
},
},
)

return {
left,
Expand Down
10 changes: 5 additions & 5 deletions src/decode-frames.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { decode } from './decode'
import { mergeUint8Array, resovleUint8Array } from './utils'
import { mergeBuffers, resovleSource } from './utils'
import { lzwDecode } from './lzw-decode'
import { deinterlace } from './deinterlace'
import { createWorker } from './create-worker'
Expand All @@ -21,7 +21,7 @@ export function decodeFrames(
source: BufferSource,
options: DecodeFramesOptions = {},
): DecodedFrame[] {
const array = resovleUint8Array(source)
const array = resovleSource(source, 'uint8Array')

const {
gif = decode(source),
Expand Down Expand Up @@ -68,8 +68,8 @@ export function decodeFrames(
const palette = localColorTable ? colorTable : globalColorTable
const transparentIndex = transparent ? transparentIndex_ : -1

const compressedData = mergeUint8Array(
...imageDataPositions.map(
const compressedData = mergeBuffers(
imageDataPositions.map(
([begin, length]) => array.subarray(begin, begin + length),
),
)
Expand Down Expand Up @@ -119,7 +119,7 @@ export function decodeFrames(
}

export async function decodeFramesInWorker(source: BufferSource, workerUrl: string): Promise<DecodedFrame[]> {
const gif = resovleUint8Array(source)
const gif = resovleSource(source, 'uint8Array')
const worker = createWorker({ workerUrl })
return await worker.call(
{ type: 'frames:decode', source: gif },
Expand Down
8 changes: 4 additions & 4 deletions src/decode-undisposed-frame.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { deinterlace } from './deinterlace'
import { lzwDecode } from './lzw-decode'
import { mergeUint8Array, resovleUint8Array } from './utils'
import { mergeBuffers, resovleSource } from './utils'
import type { Gif } from './gif'

export function decodeUndisposedFrame(source: BufferSource, gif: Gif, index: number): Uint8ClampedArray {
const array = resovleUint8Array(source)
const array = resovleSource(source, 'uint8Array')

const {
frames,
Expand Down Expand Up @@ -36,8 +36,8 @@ export function decodeUndisposedFrame(source: BufferSource, gif: Gif, index: num
const palette = localColorTable ? colorTable : globalColorTable
const transparentIndex = transparent ? transparentIndex_ : -1

const compressedData = mergeUint8Array(
...imageDataPositions.map(
const compressedData = mergeBuffers(
imageDataPositions.map(
([begin, length]) => array.subarray(begin, begin + length),
),
)
Expand Down
2 changes: 1 addition & 1 deletion src/encode-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { lzwEncode } from './lzw-encode'
import { createWriter } from './create-writer'
import type { EncodeFrameOptions } from './options'

export function encodeFrame(frame: EncodeFrameOptions): Uint8Array {
export function encodeFrame(frame: EncodeFrameOptions<Uint8ClampedArray>): Uint8Array {
const writer = createWriter()

const {
Expand Down
18 changes: 7 additions & 11 deletions src/encode.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { createEncoder } from './create-encoder'
import type { EncodeOptions } from './options'

export function encode(options: EncodeOptions): Promise<Uint8Array> {
const encoder = createEncoder(options)
export async function encode(options: EncodeOptions): Promise<Uint8Array> {
const { frames } = options

const { width, height, frames } = options
const encoder = createEncoder(options)

frames.forEach(frameOptions => {
encoder.encode({
width,
height,
...frameOptions,
})
})
for (let len = frames.length, i = 0; i < len; i++) {
await encoder.encode(frames[i])
}

return encoder.flush()
return await encoder.flush()
}
10 changes: 5 additions & 5 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Frame, Gif } from './gif'

export type EncodeFrameOptions = Partial<Frame> & {
export type EncodeFrameOptions<T = CanvasImageSource | BufferSource | string> = Partial<Frame> & {
/**
* Frame image data
*/
imageData: Uint8ClampedArray
imageData: T
}

export type EncoderOptions = Omit<Partial<Gif>, 'frames'> & {
Expand All @@ -24,12 +24,12 @@ export type EncoderOptions = Omit<Partial<Gif>, 'frames'> & {
workerNumber?: number

/**
* Max colors count
* Max colors count 2-255
*/
maxColors?: number
}

export type EncodeOptions = EncoderOptions & {
frames: EncodeFrameOptions[]
export type EncodeOptions<T = CanvasImageSource | BufferSource | string> = EncoderOptions & {
frames: EncodeFrameOptions<T>[]
}

Loading

1 comment on commit 8dbabb2

@vercel
Copy link

@vercel vercel bot commented on 8dbabb2 May 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

modern-gif – ./

modern-gif-qq15725.vercel.app
modern-gif-git-main-qq15725.vercel.app
modern-gif.vercel.app

Please sign in to comment.