@itwin/core-backend
Version:
iTwin.js backend components
171 lines • 8.71 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { expect } from "chai";
import { imageBufferFromImageSource, imageSourceFromImageBuffer } from "../ImageSourceConversion";
import { samplePngTexture } from "./imageData";
import { ImageBuffer, ImageBufferFormat, ImageSourceFormat } from "@itwin/core-common";
// samplePngTexture encodes this image:
// White Red Red
// Red Blue Red
// Red Red Green
const red = 0x000000ff;
const green = 0x0000ff00;
const blue = 0x00ff0000;
const white = 0x00ffffff;
const top = [white, red, red];
const middle = [red, blue, red];
const bottom = [red, red, green];
const samplePng = {
data: new Uint8Array(samplePngTexture.data),
format: ImageSourceFormat.Png,
};
function getPixel(img, x, y) {
const pixelSize = img.numBytesPerPixel;
const start = y * pixelSize * img.width + x * pixelSize;
let pixel = 0;
for (let offset = 0; offset < pixelSize; offset++) {
let val = img.data[start + offset];
val = (val << (offset * 8)) >>> 0;
pixel = (pixel | val) >>> 0;
}
return pixel;
}
function expectImagePixels(img, expected) {
expect(img.width * img.height).to.equal(expected.length);
const actual = [];
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
actual.push(getPixel(img, x, y));
}
}
expect(actual).to.deep.equal(expected);
}
// Make an ImageBuffer as
// White Red Red
// Red Blue Red
// Red Red Green
// If an alpha channel is requested, alpha will be 0x7f
function makeImage(wantAlpha) {
const format = wantAlpha ? ImageBufferFormat.Rgba : ImageBufferFormat.Rgb;
const pixelSize = wantAlpha ? 4 : 3;
const data = new Uint8Array(pixelSize * 9);
const lut = [white, red, red, red, blue, red, red, red, green];
for (let i = 0; i < 9; i++) {
const value = lut[i];
const s = i * pixelSize;
data[s + 0] = value & 0xff;
data[s + 1] = (value >> 8) & 0xff;
data[s + 2] = (value >> 16) & 0xff;
if (wantAlpha) {
data[s + 3] = 0x7f;
}
}
const img = ImageBuffer.create(data, format, 3);
return img;
}
function expectEqualImageBuffers(a, b, pixelTolerance = 0) {
expect(a.format).to.equal(b.format);
expect(a.width).to.equal(b.width);
expect(a.height).to.equal(b.height);
expect(a.data.length).to.equal(b.data.length);
for (let i = 0; i < a.data.length; i++) {
const x = a.data[i];
const y = b.data[i];
expect(Math.abs(x - y)).most(pixelTolerance);
}
}
function computeMaxCompressionError(compressed, original) {
expect(compressed.data.length).to.equal(original.data.length);
let max = 0;
for (let i = 0; i < compressed.data.length; i++) {
max = Math.max(max, Math.abs(compressed.data[i] - original.data[i]));
}
return max;
}
describe("ImageSource conversion", () => {
describe("imageBufferFromImageSource", () => {
it("decodes PNG", () => {
const buf = imageBufferFromImageSource({ source: samplePng });
expect(buf).not.to.be.undefined;
expect(buf.width).to.equal(3);
expect(buf.height).to.equal(3);
expect(buf.format).to.equal(ImageBufferFormat.Rgb);
expectImagePixels(buf, [...top, ...middle, ...bottom]);
});
it("preserves alpha channel if RGBA requested", () => {
const image = makeImage(true);
const source = imageSourceFromImageBuffer({ image });
const result = imageBufferFromImageSource({ source, targetFormat: ImageBufferFormat.Rgba });
expect(result.format).to.equal(ImageBufferFormat.Rgba);
expectImagePixels(result, [...top, ...middle, ...bottom].map((x) => (x | 0x7f000000) >>> 0));
});
it("strips alpha channel if RGB requested", () => {
const image = makeImage(true);
const source = imageSourceFromImageBuffer({ image });
const result = imageBufferFromImageSource({ source, targetFormat: ImageBufferFormat.Rgb });
expect(result.format).to.equal(ImageBufferFormat.Rgb);
expectImagePixels(result, [...top, ...middle, ...bottom]);
});
it("sets alpha to 255 if RGBA requested for ImageSource lacking transparency", () => {
const img = imageBufferFromImageSource({ source: samplePng, targetFormat: ImageBufferFormat.Rgba });
expect(img.format).to.equal(ImageBufferFormat.Rgba);
expectImagePixels(img, [...top, ...middle, ...bottom].map((x) => ((x | 0xff000000) >>> 0)));
});
it("defaults to RGBA IFF alpha channel is present", () => {
const transparent = imageSourceFromImageBuffer({ image: makeImage(true), targetFormat: ImageSourceFormat.Png });
expect(imageBufferFromImageSource({ source: transparent }).format).to.equal(ImageBufferFormat.Rgba);
const opaque = imageSourceFromImageBuffer({ image: makeImage(false), targetFormat: ImageSourceFormat.Png });
expect(imageBufferFromImageSource({ source: opaque }).format).to.equal(ImageBufferFormat.Rgb);
});
});
describe("imageSourceFromImageBuffer", () => {
it("encodes to specified format", () => {
const image = makeImage(false);
const png = imageSourceFromImageBuffer({ image, targetFormat: ImageSourceFormat.Png });
expect(png.format).to.equal(ImageSourceFormat.Png);
const jpeg = imageSourceFromImageBuffer({ image, targetFormat: ImageSourceFormat.Jpeg });
expect(jpeg.format).to.equal(ImageSourceFormat.Jpeg);
// PNG is lossless
expectEqualImageBuffers(imageBufferFromImageSource({ source: png }), image);
// JPEG is lossy
expectEqualImageBuffers(imageBufferFromImageSource({ source: jpeg }), image, 2);
});
it("defaults to PNG IFF alpha channel is present", () => {
const transparent = makeImage(true);
expect(imageSourceFromImageBuffer({ image: transparent }).format).to.equal(ImageSourceFormat.Png);
const opaque = makeImage(false);
expect(imageSourceFromImageBuffer({ image: opaque }).format).to.equal(ImageSourceFormat.Jpeg);
});
it("flips vertically IFF specified", () => {
const image = makeImage(false);
let source = imageSourceFromImageBuffer({ image, targetFormat: ImageSourceFormat.Png });
let output = imageBufferFromImageSource({ source });
expectImagePixels(output, [...top, ...middle, ...bottom]);
source = imageSourceFromImageBuffer({ image, targetFormat: ImageSourceFormat.Png, flipVertically: true });
output = imageBufferFromImageSource({ source });
expectImagePixels(output, [...bottom, ...middle, ...top]);
});
it("trades quality for size when encoding JPEG", () => {
const image = makeImage(false);
const low = imageSourceFromImageBuffer({ image, targetFormat: ImageSourceFormat.Jpeg, jpegQuality: 50 });
const medium = imageSourceFromImageBuffer({ image, targetFormat: ImageSourceFormat.Jpeg, jpegQuality: 75 });
const high = imageSourceFromImageBuffer({ image, targetFormat: ImageSourceFormat.Jpeg, jpegQuality: 100 });
expect(low.data.length).lessThan(medium.data.length);
expect(medium.data.length).lessThan(high.data.length);
const highImg = imageBufferFromImageSource({ source: high });
const lowImg = imageBufferFromImageSource({ source: low });
const mediumImg = imageBufferFromImageSource({ source: medium });
expect(computeMaxCompressionError(highImg, image)).to.equal(2);
const mediumError = computeMaxCompressionError(mediumImg, image);
expect(mediumError).greaterThan(2);
expect(computeMaxCompressionError(lowImg, image)).greaterThan(mediumError);
});
it("throws if image is in alpha format", () => {
const image = ImageBuffer.create(new Uint8Array([1, 2, 3, 4, 5]), ImageBufferFormat.Alpha, 5);
expect(() => imageSourceFromImageBuffer({ image })).to.throw("imageSourceFromImageBuffer cannot be used with alpha-only images");
});
});
});
//# sourceMappingURL=ImageSourceConversion.test.js.map