1.新增gif解析操作

Signed-off-by: zhoulisheng1 <zhoulisheng1@huawei.com>
This commit is contained in:
zhoulisheng1 2022-11-14 03:39:49 -08:00
parent 82ad36f241
commit 80f912ba60
11 changed files with 663 additions and 13 deletions

View File

@ -15,14 +15,15 @@
import {ImageKnifeComponent} from '@ohos/imageknife'
import {ImageKnifeOption} from '@ohos/imageknife'
import {RotateImageTransformation} from '@ohos/imageknife'
import ArkWorker from '@ohos.worker'
@Entry
@Component
struct TestGifDontAnimatePage {
private globalGifWorker:any = undefined
@State imageKnifeOption1: ImageKnifeOption =
{
loadSrc: $r('app.media.jpgSample'),
size: { width: 300, height: 300 },
size: { width: '300', height: '300' },
placeholderSrc: $r('app.media.icon_loading'),
errorholderSrc: $r('app.media.icon_failed'),
margin:{left:15,top:15,right:15,bottom:15}
@ -36,8 +37,8 @@ struct TestGifDontAnimatePage {
Button('本地资源gif')
.onClick(() => {
this.imageKnifeOption1 = {
loadSrc: $r('app.media.gifSample'),
size: { width: 300, height: 300 },
loadSrc: $r('app.media.testGif2'),
size: { width: '300', height: '300' },
placeholderSrc: $r('app.media.icon_loading'),
errorholderSrc: $r('app.media.icon_failed'),
margin: { left: 15, top: 15, right: 15, bottom: 15 }
@ -48,7 +49,7 @@ struct TestGifDontAnimatePage {
.onClick(()=>{
this.imageKnifeOption1 = {
loadSrc: $r('app.media.gifSample'),
size: { width: 300, height: 300 },
size: { width: '300', height: '300' },
placeholderSrc: $r('app.media.icon_loading'),
errorholderSrc: $r('app.media.icon_failed'),
margin:{left:15,top:15,right:15,bottom:15},
@ -63,7 +64,7 @@ struct TestGifDontAnimatePage {
.onClick(()=>{
this.imageKnifeOption1 = {
loadSrc: 'https://pic.ibaotu.com/gif/18/17/16/51u888piCtqj.gif!fwpaa70/fw/700',
size: { width: 300, height: 300 },
size: { width: '300', height: '300' },
placeholderSrc: $r('app.media.icon_loading'),
errorholderSrc: $r('app.media.icon_failed'),
margin:{left:15,top:15,right:15,bottom:15}
@ -73,7 +74,7 @@ struct TestGifDontAnimatePage {
.onClick(()=>{
this.imageKnifeOption1 = {
loadSrc: 'https://pic.ibaotu.com/gif/18/17/16/51u888piCtqj.gif!fwpaa70/fw/700',
size: { width: 300, height: 300 },
size: { width: '300', height: '300' },
placeholderSrc: $r('app.media.icon_loading'),
errorholderSrc: $r('app.media.icon_failed'),
margin:{left:15,top:15,right:15,bottom:15},
@ -91,8 +92,16 @@ struct TestGifDontAnimatePage {
}
aboutToAppear() {
console.log('aboutToAppear()')
this.globalGifWorker = new ArkWorker.Worker('entry/ets/pages/workers/gifParseWorker.ts', {
type: 'classic',
name: 'ImageKnifeParseGIF'
})
globalThis.ImageKnife.setGifWorker(this.globalGifWorker)
}
aboutToDisappear(){
if(this.globalGifWorker){
this.globalGifWorker.terminate();
}
}
}
var ImageKnife = globalThis.ImageKnife

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import arkWorker from '@ohos.worker';
import { gifHandler } from '@ohos/imageknife/src/main/ets/components/imageknife/utils/gif/worker/GifWorker'
arkWorker.parentPort.onmessage = gifHandler;

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class GIFFrame {
// 显示帧 width 宽 height 高 top上边距 left左边距
dims: {
width: number;
height: number;
top: number;
left: number
}
// 当前帧的像素数据指向的颜色数组 只为了生成patch,非必要
colorTable?: [number, number, number][]
// 当前帧到下一帧的间隔时长
delay: number
// 当前帧绘制要求 0保留 1在上一帧绘制此帧 2恢复画布背景 3.将画布恢复到绘制当前图像之前的先前状态
disposalType: number
// Uint8CampedArray颜色转换后的补片信息用于绘制 必要
patch?: Uint8ClampedArray
// drawPixelMap 如果像素转换为PixelMap后使用PixelMap展示 patch和drawPixelMap 2选1
drawPixelMap?
// 当前帧每个像素的颜色表查找索引 只为了生成patch,非必要
pixels?: number[]
// 表示透明度的可选颜色索引
transparentIndex: number
}

View File

@ -0,0 +1,159 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IParseGif } from './IParseGif'
import { GIFFrame } from './GIFFrame'
import { LoadType } from './worker/GifWorker'
import { parseBufferToFrame } from './parse/GIFParse'
import image from '@ohos.multimedia.image'
export class GIFParseImpl implements IParseGif {
parseGifs(imageinfo: ArrayBuffer, callback: (data?, err?) => void, worker?,runMainThread?:boolean) {
let resolveWorker = worker;
console.log('parseGifs resolveWorker1 is null =' + (resolveWorker == null))
if (!resolveWorker) {
resolveWorker = globalThis.ImageKnife.getGifWorker();
}
console.log('parseGifs resolveWorker2 is null =' + (resolveWorker == null))
if (!!resolveWorker && !runMainThread) {
console.log('parseGifs in worker thread!')
this.useWorkerParse(resolveWorker, imageinfo, (data, err) => {
if (err) {
callback(undefined, err)
} else {
this.createPixelMapAll(data).then((pixelmaps) => {
if (pixelmaps.length == data.length) {
for (let i = 0;i < data.length; i++) {
let frame = data[i];
frame['drawPixelMap'] = pixelmaps[i];
frame['patch'] = null;
}
callback(data, undefined)
}
}).catch(err => {
callback(undefined, err)
})
}
})
} else {
console.log('parseGifs in main thread!')
let frames = parseBufferToFrame(imageinfo)
console.log('frames length =' + frames.length)
this.createPixelMapAll(frames).then((pixelmaps) => {
if (pixelmaps.length == frames.length) {
for (let i = 0;i < frames.length; i++) {
let frame = frames[i];
frame['drawPixelMap'] = pixelmaps[i];
frame['patch'] = null;
}
console.log('parseGifs in main thread! callback is done!')
callback(frames, undefined)
}
}).catch(err => {
console.log('parseGifs in main thread! err =' + err)
callback(undefined, err)
})
}
}
private createPixelMapAll(frames): Promise<PixelMap[]> {
let promises = []
let filterCriteria = (item) => {
if (!item['drawPixelMap']) {
return true;
}
}
frames.filter(filterCriteria, frames).flatMap((frame) => {
promises.push(image.createPixelMap(frame.patch.buffer, {
'size': {
'height': frame.dims.height as number,
'width': frame.dims.width as number
}
}))
})
return Promise.all(promises)
}
private useWorkerParse(worker: any, buffer: ArrayBuffer, callback: (data?, err?) => void) {
worker.onerror = function (data) {
callback(undefined, data)
}
worker.onmessageerror = function (event) {
callback(undefined, event)
}
worker.onexit = function () {
console.log('gifWork worker.onexit!')
}
worker.onmessage = (e) => {
var data = e.data;
switch (data.type) {
case LoadType.loadBufferByWorker:
let pages = data.data;
if (this.gifDecodeCorrect(pages)) {
let images = this.recDecodedData(pages);
callback(images, undefined)
} else {
callback(undefined, 'GIF Worker Decoder Data Is Error!')
}
break;
default:
break
}
}
var obj = { type: LoadType.loadBufferByWorker, data: buffer }
worker.postMessage(obj, [buffer])
}
private gifDecodeCorrect(frames) {
if (
(frames.patch.length == frames.dims.length) &&
(frames.patch.length == frames.delay.length) &&
(frames.patch.length == frames.disposalType.length) &&
(frames.patch.length == frames.patch.length) &&
(frames.patch.length == frames.transparentIndex.length)
) {
return true;
}
return false;
}
// 子线程数据回传处理
private recDecodedData(pages): GIFFrame[] {
let images = []
for (let i = 0; i < pages.patch.length; i++) {
let frame = {}
frame['dims'] = pages.dims[i]
pages.dims[i] = null
frame['delay'] = pages.delay[i]
pages.delay[i] = null
frame['disposalType'] = pages.disposalType[i]
pages.disposalType[i] = null
let uint8ClampedArray = new Uint8ClampedArray(pages.patch[i])
frame['patch'] = uint8ClampedArray
pages.patch[i] = null
frame['transparentIndex'] = pages.transparentIndex[i]
pages.transparentIndex[i] = null
images.push(frame)
}
pages = null
return images;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Huawei Device Co., Ltd.
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@ -12,7 +12,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export class PixelMapPack {
pixelMap:PixelMap
export interface IParseGif{
// gif解析
parseGifs(imageinfo:ArrayBuffer,callback:(data?,err?)=>void,worker?)
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { decompressFrames, parseGIF } from './index'
import { fixLoseGCE} from '../utils/ParseHelperUtils'
import { GIFFrame } from '../GIFFrame'
export function parseBufferToFrame(arraybuffer: ArrayBuffer): GIFFrame[] {
let gif = parseGIF(arraybuffer)
fixLoseGCE(gif)
let origins = decompressFrames(gif, true)
return origins as GIFFrame[];
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Deinterlace function from https://github.com/shachaf/jsgif
*/
export function deinterlace(pixels, width) {
var newPixels = new Array(pixels.length);
var rows = pixels.length / width;
var cpRow = function cpRow(toRow, fromRow) {
var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
}; // See appendix E.
var offsets = [0, 4, 2, 1];
var steps = [8, 8, 4, 2];
var fromRow = 0;
for (var pass = 0; pass < 4; pass++) {
for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
cpRow(toRow, fromRow);
fromRow++;
}
}
return newPixels;
};

View File

@ -0,0 +1,114 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var _gif = _interopRequireDefault(require("js-binary-schema-parser/lib/schemas/gif"));
var _jsBinarySchemaParser = require("js-binary-schema-parser");
var _uint = require("js-binary-schema-parser/lib/parsers/uint8");
import {deinterlace} from './deinterlace'
import {lzw} from './lzw'
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
export function parseGIF(arrayBuffer) {
var byteData = new Uint8Array(arrayBuffer);
return (0, _jsBinarySchemaParser.parse)((0, _uint.buildStream)(byteData), _gif["default"]);
};
export function generatePatch(image) {
var totalPixels = image.pixels.length;
var patchData = new Uint8ClampedArray(totalPixels * 4);
for (var i = 0; i < totalPixels; i++) {
var pos = i * 4;
var colorIndex = image.pixels[i];
var color = image.colorTable[colorIndex] || [0, 0, 0];
patchData[pos] = color[2];
patchData[pos + 1] = color[1];
patchData[pos + 2] = color[0];
patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;
}
return patchData;
};
export function decompressFrame(frame, gct, buildImagePatch) {
if (!frame.image) {
return;
}
var image = frame.image; // get the number of pixels
var totalPixels = image.descriptor.width * image.descriptor.height; // do lzw decompression
var pixels = lzw(image.data.minCodeSize, image.data.blocks, totalPixels); // deal with interlacing if necessary
if (image.descriptor.lct.interlaced) {
pixels = deinterlace(pixels, image.descriptor.width);
}
var resultImage = {
pixels: pixels,
dims: {
top: frame.image.descriptor.top,
left: frame.image.descriptor.left,
width: frame.image.descriptor.width,
height: frame.image.descriptor.height
}
}; // color table
if (image.descriptor.lct && image.descriptor.lct.exists) {
resultImage['colorTable'] = image.lct;
} else {
resultImage['colorTable'] = gct;
} // add per frame relevant gce information
if (frame.gce) {
resultImage['delay'] = (frame.gce.delay || 10) * 10; // convert to ms
resultImage['disposalType'] = frame.gce.extras.disposal; // transparency
if (frame.gce.extras.transparentColorGiven) {
resultImage['transparentIndex'] = frame.gce.transparentColorIndex;
}
} // create canvas usable imagedata if desired
if (buildImagePatch) {
resultImage['patch'] = generatePatch(resultImage);
resultImage['colorTable'] = null
resultImage['transparentIndex'] = null
resultImage['pixels'] = null
}
return resultImage;
};
export function decompressFrames(parsedGif, buildImagePatches) {
return parsedGif.frames.filter(function (f) {
return f.image;
}).map(function (f) {
return decompressFrame(f, parsedGif.gct, buildImagePatches);
});
};

View File

@ -0,0 +1,139 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* javascript port of java LZW decompression
* Original java author url: https://gist.github.com/devunwired/4479231
*/
export function lzw(minCodeSize, data, pixelCount) {
var MAX_STACK_SIZE = 4096;
var nullCode = -1;
var npix = pixelCount;
var available;
var clear;
var codeMask;
var codeSize;
var endOfInformation;
var inCode;
var oldCode;
var bits;
var code;
var i;
var datum;
var dataSize;
var first;
var top;
var bi;
var pi;
var dstPixels = new Array(pixelCount);
var prefix = new Array(MAX_STACK_SIZE);
var suffix = new Array(MAX_STACK_SIZE);
var pixelStack = new Array(MAX_STACK_SIZE + 1); // Initialize GIF data stream decoder.
dataSize = minCodeSize;
clear = 1 << dataSize;
endOfInformation = clear + 1;
available = clear + 2;
oldCode = nullCode;
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
for (code = 0; code < clear; code++) {
prefix[code] = 0;
suffix[code] = code;
} // Decode GIF pixel stream.
var datum, bits, count, first, top, pi, bi;
datum = bits = count = first = top = pi = bi = 0;
for (i = 0; i < npix;) {
if (top === 0) {
if (bits < codeSize) {
// get the next byte
datum += data[bi] << bits;
bits += 8;
bi++;
continue;
} // Get the next code.
code = datum & codeMask;
datum >>= codeSize;
bits -= codeSize; // Interpret the code
if (code > available || code == endOfInformation) {
break;
}
if (code == clear) {
// Reset decoder.
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
available = clear + 2;
oldCode = nullCode;
continue;
}
if (oldCode == nullCode) {
pixelStack[top++] = suffix[code];
oldCode = code;
first = code;
continue;
}
inCode = code;
if (code == available) {
pixelStack[top++] = first;
code = oldCode;
}
while (code > clear) {
pixelStack[top++] = suffix[code];
code = prefix[code];
}
first = suffix[code] & 0xff;
pixelStack[top++] = first; // add a new string to the table, but only if space is available
// if not, just continue with current table until a clear code is found
// (deferred clear code implementation as per GIF spec)
if (available < MAX_STACK_SIZE) {
prefix[available] = oldCode;
suffix[available] = first;
available++;
if ((available & codeMask) === 0 && available < MAX_STACK_SIZE) {
codeSize++;
codeMask += available;
}
}
oldCode = inCode;
} // Pop a pixel off the pixel stack.
top--;
dstPixels[pi++] = pixelStack[top];
i++;
}
for (i = pi; i < npix; i++) {
dstPixels[i] = 0; // clear missing pixels
}
return dstPixels;
};

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function fixLoseGCE(gif) {
let currentGce = null;
for (const frame of gif.frames) {
currentGce = frame.gce ? frame.gce : currentGce;
// fix loosing graphic control extension for same frames
if ("image" in frame && !("gce" in frame)) {
frame.gce = currentGce;
}
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (C) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import arkWorker from '@ohos.worker';
import { parseBufferToFrame } from '../parse/GIFParse'
export enum LoadType {
loadBufferByWorker = "loadBufferByWorker"
}
// Send or Receive Format Data Such as: {type: yourResolveType, data: yourDataJson, error?: yourErrorInfo }
export function gifHandler(e) {
let data = e.data;
switch (data.type) {
case LoadType.loadBufferByWorker:
loadBufferByWorker(data.data, data.type);
break;
default:
break
}
function loadBufferByWorker(buffer: ArrayBuffer, recType: string) {
let images = parseBufferToFrame(buffer);
let dimss = [];
let delays = [];
let disposalTypes = [];
let patchs = [];
let transparentIndexs = [];
for (let i = 0; i < images.length; i++) {
dimss.push(images[i].dims)
delays.push(images[i].delay)
disposalTypes.push(images[i].disposalType)
patchs.push(images[i].patch.buffer)
transparentIndexs.push(images[i].transparentIndex)
}
let frame = {
dims: dimss,
// 当前帧到下一帧的间隔时长
delay: delays,
// 当前帧绘制要求 0保留 1在上一帧绘制此帧 2恢复画布背景 3.将画布恢复到绘制当前图像之前的先前状态
disposalType: disposalTypes,
// Uint8CampedArray颜色转换后的补片信息用于绘制
patch: patchs,
// 表示透明度的可选颜色索引
transparentIndex: transparentIndexs
}
let dataObj = { type: recType, data: frame }
arkWorker.parentPort.postMessage(dataObj, patchs);
}
}