Skip to content

Commit

Permalink
sCAL support
Browse files Browse the repository at this point in the history
Part of #1
  • Loading branch information
Tyriar committed Jan 21, 2022
1 parent 5937d4f commit 724703a
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 11 deletions.
72 changes: 72 additions & 0 deletions src/chunks/chunk_sCAL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @license
* Copyright (c) 2022 Daniel Imms <http://www.growingwiththeweb.com>
* Released under MIT license. See LICENSE in the project root for details.
*/

import { assertChunkDataLengthGte, assertChunkPrecedes, assertChunkSinglular, createChunkDecodeWarning } from '../assert.js';
import { readText } from '../text.js';
import { ChunkPartByteLength, IDecodeContext, IPngChunk, IPngHeaderDetails, IPngMetadataPhysicalScaleOfImageSubject, KnownChunkTypes } from '../types.js';

/**
* `sCAL` Physical scale of image subject
*
* Spec: https://www.w3.org/TR/PNG/#11sCAL
*/
export function parseChunk(ctx: IDecodeContext, header: IPngHeaderDetails, chunk: IPngChunk): IPngMetadataPhysicalScaleOfImageSubject {
assertChunkSinglular(ctx, chunk);
assertChunkPrecedes(ctx, chunk, KnownChunkTypes.IDAT);
assertChunkDataLengthGte(ctx, chunk, 4);

// Format:
// Unit specifier: 1 byte
// Pixel width: 1 or more bytes (ASCII floating-point)
// Null separator: 1 byte
// Pixel height: 1 or more bytes (ASCII floating-point)
const chunkDataOffset = chunk.offset + ChunkPartByteLength.Length + ChunkPartByteLength.Type;
const maxOffset = chunkDataOffset + chunk.dataLength; // Ensures reading outside this chunk is not allowed
let offset = chunkDataOffset;
const textDecoder = new TextDecoder('latin1');

const unitTypeByte = ctx.view.getUint8(offset);
let unitType: 'meter' | 'radian';
switch (unitTypeByte) {
case 0: unitType = 'meter'; break;
case 1: unitType = 'radian'; break;
default: throw createChunkDecodeWarning(chunk, 'Invalid sCAL unit type', offset);
}
offset++;

let readResult: { bytesRead: number, text: string };
readResult = readText(ctx, chunk, textDecoder, undefined, offset, maxOffset, true);
offset += readResult.bytesRead;
if (!isValidFloatingPoint(readResult.text)) {
throw createChunkDecodeWarning(chunk, `Invalid character in floating point number ("${readResult.text}")`, offset);
}
const x = parseFloat(readResult.text);

readResult = readText(ctx, chunk, textDecoder, undefined, offset, maxOffset, false);
offset += readResult.bytesRead;
if (!isValidFloatingPoint(readResult.text)) {
throw createChunkDecodeWarning(chunk, `Invalid character in floating point number ("${readResult.text}")`, offset);
}
const y = parseFloat(readResult.text);

if (x < 0 || y < 0) {
throw createChunkDecodeWarning(chunk, `Values cannot be negative (${x}, ${y})`, offset);
}

return {
type: 'sCAL',
pixelsPerUnit: { x, y },
unitType
};
}

function isValidFloatingPoint(text: string) {
return (
text.match(/^[+-]?[0-9]+\.[0-9]+([eE][+-][0-9]+)?$/) ||
text.match(/^[+-]?[0-9]+\.?([eE][+-][0-9]+)?$/) ||
text.match(/^[+-]?\.[0-9]+([eE][+-][0-9]+)?$/)
);
}
2 changes: 2 additions & 0 deletions src/pngDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const allLazyChunkTypes: ReadonlyArray<string> = Object.freeze([
KnownChunkTypes.tIME,
KnownChunkTypes.pHYs,
KnownChunkTypes.sBIT,
KnownChunkTypes.sCAL,
KnownChunkTypes.sPLT,
KnownChunkTypes.sRGB,
KnownChunkTypes.tEXt,
Expand All @@ -75,6 +76,7 @@ function getChunkDecoder(type: KnownChunkTypes): Promise<{ parseChunk: (ctx: IDe
case KnownChunkTypes.tIME: return import(`./chunks/chunk_tIME.js`);
case KnownChunkTypes.pHYs: return import(`./chunks/chunk_pHYs.js`);
case KnownChunkTypes.sBIT: return import(`./chunks/chunk_sBIT.js`);
case KnownChunkTypes.sCAL: return import(`./chunks/chunk_sCAL.js`);
case KnownChunkTypes.sPLT: return import(`./chunks/chunk_sPLT.js`);
case KnownChunkTypes.sRGB: return import(`./chunks/chunk_sRGB.js`);
case KnownChunkTypes.tEXt: return import(`./chunks/chunk_tEXt.js`);
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export {
IPngMetadataInternationalTextualData,
IPngMetadataLastModificationTime,
IPngMetadataPhysicalPixelDimensions,
IPngMetadataPhysicalScaleOfImageSubject,
IPngMetadataSignificantBits,
IPngMetadataStandardRgbColorSpace,
IPngMetadataSuggestedPalette,
Expand Down
38 changes: 29 additions & 9 deletions test/imagetestsuite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ describe('Image Test Suite', () => {
['18bd8bf75e7a9b40b961dd501654ce0e', 'should decode with warnings', { expectedWarnings: ['hIST: Must precede IDAT'] }],
], imageTestSuiteRoot);
createTests([
['1b9a48cf04466108f6f2d225d100edbf', 'sCAL must precede IDAT', true], // TODO: Support sCAL?
['1b9a48cf04466108f6f2d225d100edbf', 'sCAL must precede IDAT', { shouldThrow: 'sCAL: Must precede IDAT', strictMode: true }],
['1b9a48cf04466108f6f2d225d100edbf', 'sCAL must precede IDAT', { expectedWarnings: [
'pHYs: Must precede IDAT',
'sCAL: Must precede IDAT'
], expectedInfo: [
'Unrecognized chunk type "oFFs"',
'Unrecognized chunk type "pCAL"'
], expectedDimensions: { width: 91, height: 69 } }],
], imageTestSuiteRoot);
createTests([
['31e3bc3eb811cff582b5feee2494fed8', 'sBIT must precede IDAT', { shouldThrow: 'sBIT: Must precede IDAT', strictMode: true }],
Expand Down Expand Up @@ -68,10 +75,12 @@ describe('Image Test Suite', () => {
], imageTestSuiteRoot);
createTests([
['ed5f2464fcaadd4e0a5e905e3ac41ad5', 'pHYs must precede IDAT', { shouldThrow: 'pHYs: Must precede IDAT', strictMode: true }],
['ed5f2464fcaadd4e0a5e905e3ac41ad5', 'should decode with warnings', { expectedWarnings: ['pHYs: Must precede IDAT'], expectedInfo: [
['ed5f2464fcaadd4e0a5e905e3ac41ad5', 'should decode with warnings', { expectedWarnings: [
'pHYs: Must precede IDAT',
'sCAL: Must precede IDAT'
], expectedInfo: [
'Unrecognized chunk type "oFFs"',
'Unrecognized chunk type "pCAL"',
'Unrecognized chunk type "sCAL"'
], expectedDimensions: { width: 91, height: 69 } }],
], imageTestSuiteRoot);
createTests([
Expand All @@ -97,7 +106,11 @@ describe('Image Test Suite', () => {
['13f665c09e4b03cdbe2fff3015ec8aa7', 'should decode with warnings', { expectedWarnings: ['bKGD: Multiple bKGD chunks not allowed'] }],
], imageTestSuiteRoot);
createTests([
['1bcc34d49e56a2fba38490db206328b8', 'multiple sCAL not allowed', true], // TODO: Support sCAL?
['1bcc34d49e56a2fba38490db206328b8', 'multiple sCAL not allowed', { shouldThrow: 'sCAL: Multiple sCAL chunks not allowed', strictMode: true }],
['1bcc34d49e56a2fba38490db206328b8', 'should decode with warnings', { expectedWarnings: ['sCAL: Multiple sCAL chunks not allowed'], expectedInfo: [
'Unrecognized chunk type "oFFs"',
'Unrecognized chunk type "pCAL"'
], expectedDimensions: { width: 91, height: 69 } }],
], imageTestSuiteRoot);
createTests([
['463d3570f92a6b6ecba0cc4fd9a7a384', 'multiple PLTE not allowed', { shouldThrow: 'PLTE: Multiple PLTE chunks not allowed', strictMode: true }],
Expand Down Expand Up @@ -197,10 +210,15 @@ describe('Image Test Suite', () => {
['4389427591c18bf36e748172640862c3', 'invalid sTER layout mode', true], // TODO: Support sTER?
], imageTestSuiteRoot);
createTests([
['6399623892b45aa4901aa6e702c7a62d', 'invalid negative sCAL value(s)', true], // TODO: Support sCAL?
['6399623892b45aa4901aa6e702c7a62d', 'invalid negative sCAL value(s)', { shouldThrow: 'sCAL: Values cannot be negative (1, -1)', strictMode: true }],
['6399623892b45aa4901aa6e702c7a62d', 'should decode with warnings', { expectedWarnings: ['sCAL: Values cannot be negative (1, -1)'] }],
], imageTestSuiteRoot);
createTests([
['8905ba870cd5d3327a8310fa437aa076', 'invalid character (\'Q\' = 0x51) in sCAL', true], // TODO: Support sCAL?
['8905ba870cd5d3327a8310fa437aa076', 'invalid character (\'Q\' = 0x51) in sCAL', { shouldThrow: 'sCAL: Invalid character in floating point number ("Q.527777777778e-04")', strictMode: true }],
['8905ba870cd5d3327a8310fa437aa076', 'should decode with warnings', { expectedWarnings: ['sCAL: Invalid character in floating point number ("Q.527777777778e-04")'], expectedInfo: [
'Unrecognized chunk type "oFFs"',
'Unrecognized chunk type "pCAL"'
], expectedDimensions: { width: 91, height: 69 } }],
], imageTestSuiteRoot);
createTests([
['7ce702ec69b7af26b3218d1278520bce', 'IHDR: Filter method "128" is not valid', { shouldThrow: 'IHDR: Filter method "128" is not valid', strictMode: true }],
Expand Down Expand Up @@ -513,10 +531,12 @@ describe('Image Test Suite', () => {
['bd927c8547634cdbdd22af0afe818a9b', 'should throw', { shouldThrow: true }],
], imageTestSuiteRoot);
createTests([
['bf203e765c98b12f6c2b2c33577c730d', 'should throw', { shouldThrow: 'pHYs: Must precede IDAT', strictMode: true }],
['bf203e765c98b12f6c2b2c33577c730d', 'should throw', { expectedWarnings: ['pHYs: Must precede IDAT'], expectedInfo: [
['bf203e765c98b12f6c2b2c33577c730d', 'should throw', { shouldThrow: 'sCAL: Must precede IDAT', strictMode: true }],
['bf203e765c98b12f6c2b2c33577c730d', 'should throw', { expectedWarnings: [
'pHYs: Must precede IDAT',
'sCAL: Must precede IDAT'
], expectedInfo: [
'Unrecognized chunk type "pCAL"',
'Unrecognized chunk type "sCAL"',
'Unrecognized chunk type "oFFs"'
], expectedDimensions: { width: 91, height: 69 } }],
], imageTestSuiteRoot);
Expand Down
25 changes: 24 additions & 1 deletion typings/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export const enum KnownChunkTypes {
tIME = 'tIME',
pHYs = 'pHYs',
sBIT = 'sBIT',
sCAL = 'sCAL',
sPLT = 'sPLT',
sRGB = 'sRGB',
tEXt = 'tEXt',
Expand Down Expand Up @@ -260,6 +261,7 @@ export type PngMetadata =
IPngMetadataInternationalTextualData |
IPngMetadataLastModificationTime |
IPngMetadataPhysicalPixelDimensions |
IPngMetadataPhysicalScaleOfImageSubject |
IPngMetadataSignificantBits |
IPngMetadataStandardRgbColorSpace |
IPngMetadataSuggestedPalette |
Expand Down Expand Up @@ -485,7 +487,7 @@ export interface IPngMetadataLastModificationTime {
/**
* A metadata entry that defines the intended pixel size or aspect ratio of the image.
*/
export interface IPngMetadataPhysicalPixelDimensions {
export interface IPngMetadataPhysicalPixelDimensions {
/**
* The type of metadata, this is typically the name of the chunk from which is originates.
*/
Expand All @@ -502,6 +504,27 @@ export interface IPngMetadataPhysicalPixelDimensions {
unitType: 'meter' | 'unknown';
}

/**
* A metadata entry that defines the physical scale of the subject within the image. This is often
* used for maps, floor plans, etc.
*/
export interface IPngMetadataPhysicalScaleOfImageSubject {
/**
* The type of metadata, this is typically the name of the chunk from which is originates.
*/
type: 'sCAL';

/**
* The number of pixels per unit for each dimension.
*/
pixelsPerUnit: { x: number, y: number };

/**
* The unit type of the dimensions.
*/
unitType: 'meter' | 'radian';
}

/**
* A metadata entry that defines a bit depth less than or equal to the image's bit depth which
* allows potentially storing bit depths not equal to 1, 2, 4, 8 or 16 within pngs. Since the
Expand Down

0 comments on commit 724703a

Please sign in to comment.