Skip to content

Commit

Permalink
feat: add workerNumber options
Browse files Browse the repository at this point in the history
  • Loading branch information
qq15725 committed Feb 19, 2023
1 parent d2a844b commit 68facd7
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 151 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ window.fetch('https://raw.githubusercontent.com/qq15725/modern-gif/master/test/a

See the [gif.ts](src/gif.ts)

## Encode Options

See the [options.ts](src/options.ts)

## Specifications

[GIF89a Spec](https://www.w3.org/Graphics/GIF/spec-gif89a.txt)
1 change: 0 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
workerUrl: '/dist/worker.js',
width: gif.width,
height: gif.height,
colorTableGeneration: 'NeuQuant',
})
// eslint-disable-next-line no-console
console.time('encode')
Expand Down
6 changes: 2 additions & 4 deletions src/create-color-table-by-mmcq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ const sigbits = 5
const rshift = 8 - sigbits
const maxIterations = 1000
const fractByPopulations = 0.75
const naturalOrder = (a: number, b: number) => (a < b) ? -1 : ((a > b) ? 1 : 0)

function getColorIndex(r: number, g: number, b: number) {
return (r << (2 * sigbits)) + (g << sigbits) + b
}
const naturalOrder = (a: number, b: number) => (a < b) ? -1 : ((a > b) ? 1 : 0)
const getColorIndex = (r: number, g: number, b: number) => (r << (2 * sigbits)) + (g << sigbits) + b

function createVBox(r1: number, r2: number, g1: number, g2: number, b1: number, b2: number, histogram: number[]) {
let volume = 0
Expand Down
3 changes: 2 additions & 1 deletion src/create-color-table-by-neuquant.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RGB } from './gif'

/* NeuQuant Neural-Net Quantization Algorithm
/*
* NeuQuant Neural-Net Quantization Algorithm
* ------------------------------------------
*
* Copyright (c) 1994 Anthony Dekker
Expand Down
156 changes: 48 additions & 108 deletions src/create-encoder.ts
Original file line number Diff line number Diff line change
@@ -1,133 +1,59 @@
import {
EXTENSION,
EXTENSION_APPLICATION,
EXTENSION_APPLICATION_BLOCK_SIZE,
SIGNATURE,
TRAILER,
} from './utils'
import { createWriter } from './create-writer'
import { encodeFrame } from './encode-frame'
import type { Frame, GIF } from './gif'
import { encodeBasicInfo } from './encode-basic-info'
import type { EncodeFrameOptions, EncoderOptions } from './options'

export interface Encoder {
encode: (frame: Partial<Frame>) => void
encode: (frame: EncodeFrameOptions) => void
flush: () => Promise<Uint8Array>
}

export function createEncoder(options: Partial<GIF>): Encoder {
const gif = {
colorTableGeneration: 'NeuQuant',
version: '89a',
width: 0,
height: 0,
backgroundColorIndex: 0,
pixelAspectRatio: 0,
looped: true,
...options,
} as GIF

if (gif.width <= 0 || gif.width > 65535) throw new Error('Width invalid.')
if (gif.height <= 0 || gif.height > 65535) throw new Error('Height invalid.')

// Handling of global color table size
let colorTableSize = 0
if (gif.colorTable?.length) {
let colorTableLength = gif.colorTable.length
if (colorTableLength < 2 || colorTableLength > 256 || colorTableLength & (colorTableLength - 1)) {
throw new Error('Invalid color table length, must be power of 2 and 2 .. 256.')
}
// eslint-disable-next-line no-cond-assign
while (colorTableLength >>= 1) ++colorTableSize
colorTableLength = 1 << colorTableSize
--colorTableSize
if (gif.backgroundColorIndex >= colorTableLength) {
throw new Error('Background index out of range.')
}
if (gif.backgroundColorIndex === 0) {
throw new Error('Background index explicitly passed as 0.')
}
}

const writer = createWriter()

export function createEncoder(options: EncoderOptions): Encoder {
const {
writeByte,
writeBytes,
writeUnsigned,
writeString,
flush,
} = writer

function writeBaseInfo() {
// Header
writeString(SIGNATURE)
writeString(gif.version)

// Logical Screen Descriptor
writeUnsigned(gif.width)
writeUnsigned(gif.height)
// <Packed Fields>
// 1 : global color table flag = 1
// 2-4 : color resolution = 7
// 5 : global color table sort flag = 0
// 6-8 : global color table size
writeByte(parseInt(`${ colorTableSize ? 1 : 0 }1110${ colorTableSize.toString(2).padStart(3, '0') }`, 2))
writeByte(gif.backgroundColorIndex) // background color index
writeByte(gif.pixelAspectRatio) // pixel aspect ratio - assume 1:1

// Global Color Table
writeBytes(gif.colorTable?.flat() ?? [])
workerUrl,
workerNumber = 1,
} = options

// Netscape block
if (gif.looped) {
writeByte(EXTENSION) // extension introducer
writeByte(EXTENSION_APPLICATION) // app extension label
writeByte(EXTENSION_APPLICATION_BLOCK_SIZE) // block size
writeString('NETSCAPE2.0') // app id + auth code
writeByte(3) // sub-block size
writeByte(1) // loop sub-block id
writeUnsigned(gif.loopCount ?? 0) // loop count (extra iterations, 0=repeat forever)
writeByte(0) // block terminator
}
}

let frameIndex = 0
let lastIndex = 0
let flushResolve: any | undefined
const encodeing = new Map<number, boolean>()
const encodeing = new Set<number>()
let frames: Uint8Array[] = []
let framesDataLength = 0

function onEncoded(index: number) {
encodeing.delete(index)
if (!encodeing.size) {
flushResolve?.()
}
}

function reset() {
frameIndex = 0
encodeing.clear()
writeBaseInfo()
!encodeing.size && flushResolve?.()
}

writeBaseInfo()
const basicInfo = encodeBasicInfo(options)

const worker = gif.workerUrl ? new Worker(gif.workerUrl) : undefined
if (worker) {
const workers = [...new Array(workerUrl ? workerNumber : 0)].map(() => {
const worker = new Worker(workerUrl!)
worker.onmessage = event => {
const { index, data } = event.data
writeBytes(data)
frames[index] = data
framesDataLength += data.length
onEncoded(index)
}
worker.onmessageerror = event => onEncoded(event.data.index)
}
return worker
})

return {
encode: (frame: Partial<Frame>) => {
const index = frameIndex++
if (worker && frame.imageData?.buffer) {
encodeing.set(index, true)
worker.postMessage({ index, frame }, [frame.imageData.buffer])
encode: frame => {
const index = lastIndex++
if (workers.length && frame.imageData.buffer) {
encodeing.add(index)
workers[index & (workers.length - 1)].postMessage(
{ index, frame },
[frame.imageData.buffer],
)
} else {
writeBytes(encodeFrame(frame))
const data = encodeFrame(frame)
frames[index] = data
framesDataLength += data.length
onEncoded(index)
}
},
Expand All @@ -136,13 +62,27 @@ export function createEncoder(options: Partial<GIF>): Encoder {
const timer = setTimeout(() => flushResolve?.(), 30000)
flushResolve = () => {
timer && clearTimeout(timer)

const data = new Uint8Array(basicInfo.length + framesDataLength + 1)
data.set(basicInfo)
let offset = basicInfo.length
frames.forEach(frame => {
data.set(frame, offset)
offset += frame.length
})
// Trailer
writeByte(TRAILER)
const data = flush()
reset()
data[data.length - 1] = TRAILER
resolve(data)

// reset
lastIndex = 0
flushResolve = undefined
encodeing.clear()
frames = []
framesDataLength = 0
}
!encodeing.size && flushResolve?.()
})
},
}
} as Encoder
}
4 changes: 2 additions & 2 deletions src/decode-frame-but-undisposed.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { deinterlace } from './deinterlace'
import { lzwDecode } from './lzw-decode'
import type { GIF } from './gif'
import type { Gif } from './gif'

export function decodeFrameButUndisposed(gifData: Uint8Array, gif: GIF, index: number): ImageData {
export function decodeFrameButUndisposed(gifData: Uint8Array, gif: Gif, index: number): ImageData {
const {
width: gifWidth,
height: gifHeight,
Expand Down
4 changes: 2 additions & 2 deletions src/decode-frame.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { decodeFrames } from './decode-frames'
import type { GIF } from './gif'
import type { Gif } from './gif'

export function decodeFrame(gifData: Uint8Array, gif: GIF, index: number): ImageData {
export function decodeFrame(gifData: Uint8Array, gif: Gif, index: number): ImageData {
return decodeFrames(gifData, gif, [0, Math.max(index, 0)]).pop()!
}
4 changes: 2 additions & 2 deletions src/decode-frames.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { decodeFrameButUndisposed } from './decode-frame-but-undisposed'
import type { Frame, GIF } from './gif'
import type { Frame, Gif } from './gif'

export function decodeFrames(gifData: Uint8Array, gif: GIF, range?: number[]): ImageData[] {
export function decodeFrames(gifData: Uint8Array, gif: Gif, range?: number[]): ImageData[] {
const { frames, width: gifWidth, height: gifHeight } = gif
const rangeFrames = range ? frames.slice(range[0], range[1] + 1) : frames
const pixels = new Uint8ClampedArray(gifWidth * gifHeight * 4)
Expand Down
6 changes: 3 additions & 3 deletions src/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import {
consoleWarn,
} from './utils'
import { createReader } from './create-reader'
import type { Application, Frame, GIF, GraphicControl, PlainText } from './gif'
import type { Application, Frame, Gif, GraphicControl, PlainText } from './gif'

export function decode(data: Uint8Array): GIF {
const gif = {} as GIF
export function decode(data: Uint8Array): Gif {
const gif = {} as Gif

const {
getCursor,
Expand Down
83 changes: 83 additions & 0 deletions src/encode-basic-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { EXTENSION, EXTENSION_APPLICATION, EXTENSION_APPLICATION_BLOCK_SIZE, SIGNATURE } from './utils'
import { createWriter } from './create-writer'
import type { EncoderOptions } from './options'

export function encodeBasicInfo(options: EncoderOptions) {
const gif = {
version: '89a',
looped: true,
loopCount: 0,
width: 0,
height: 0,
colorTableSize: 0,
backgroundColorIndex: 0,
pixelAspectRatio: 0,
...options,
}

if (gif.width <= 0 || gif.width > 65535) throw new Error('Width invalid.')
if (gif.height <= 0 || gif.height > 65535) throw new Error('Height invalid.')

// Handling of global color table size
let colorTableSize = 0
if (gif.colorTable?.length) {
let colorTableLength = gif.colorTable.length
if (colorTableLength < 2 || colorTableLength > 256 || colorTableLength & (colorTableLength - 1)) {
throw new Error('Invalid color table length, must be power of 2 and 2 .. 256.')
}
// eslint-disable-next-line no-cond-assign
while (colorTableLength >>= 1) ++colorTableSize
colorTableLength = 1 << colorTableSize
gif.colorTableSize = --colorTableSize
if (gif.backgroundColorIndex >= colorTableLength) {
throw new Error('Background index out of range.')
}
if (gif.backgroundColorIndex === 0) {
throw new Error('Background index explicitly passed as 0.')
}
}

// max length 32 + 256 * 3
const writer = createWriter()

const {
writeByte,
writeBytes,
writeUnsigned,
writeString,
flush,
} = writer

// Header
writeString(SIGNATURE)
writeString(gif.version)

// Logical Screen Descriptor
writeUnsigned(gif.width)
writeUnsigned(gif.height)
// <Packed Fields>
// 1 : global color table flag = 1
// 2-4 : color resolution = 7
// 5 : global color table sort flag = 0
// 6-8 : global color table size
writeByte(parseInt(`${ gif.colorTableSize ? 1 : 0 }1110${ gif.colorTableSize.toString(2).padStart(3, '0') }`, 2))
writeByte(gif.backgroundColorIndex) // background color index
writeByte(gif.pixelAspectRatio) // pixel aspect ratio - assume 1:1

// Global Color Table
writeBytes(gif.colorTable?.flat() ?? [])

// Netscape block
if (gif.looped) {
writeByte(EXTENSION) // extension introducer
writeByte(EXTENSION_APPLICATION) // app extension label
writeByte(EXTENSION_APPLICATION_BLOCK_SIZE) // block size
writeString('NETSCAPE2.0') // app id + auth code
writeByte(3) // sub-block size
writeByte(1) // loop sub-block id
writeUnsigned(gif.loopCount) // loop count (extra iterations, 0=repeat forever)
writeByte(0) // block terminator
}

return flush()
}
Loading

0 comments on commit 68facd7

Please sign in to comment.