Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import {getCatalogImages, getImageSet} from '../assetCatalogIOS';

const path = require('node:path');

jest.dontMock('../assetCatalogIOS');

beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
});

afterEach(() => {
jest.restoreAllMocks();
});

function makeAsset(scales: Array<number>) {
return {
__packager_asset: true,
fileSystemLocation: '/project/img',
httpServerLocation: '/assets/img',
width: 100,
height: 100,
scales,
files: scales.map(
scale => `/project/img/logo${scale === 1 ? '' : `@${scale}x`}.png`,
),
hash: 'hash',
name: 'logo',
type: 'png',
};
}

describe('getCatalogImages', () => {
test('pairs each standard scale with its file', () => {
const asset = makeAsset([1, 2, 3]);
expect(getCatalogImages(asset)).toEqual([
{scale: 1, src: '/project/img/logo.png'},
{scale: 2, src: '/project/img/logo@2x.png'},
{scale: 3, src: '/project/img/logo@3x.png'},
]);
});

test('skips non-standard scales without shifting file pairing', () => {
// Regression test: filtering scales without filtering files used to
// associate the 2x rendition with the 1.5x file.
const asset = makeAsset([1, 1.5, 2, 3]);
expect(getCatalogImages(asset)).toEqual([
{scale: 1, src: '/project/img/logo.png'},
{scale: 2, src: '/project/img/logo@2x.png'},
{scale: 3, src: '/project/img/logo@3x.png'},
]);
});

test('maps a fractional-only asset into the nearest valid slot', () => {
const asset = makeAsset([1.5]);
expect(getCatalogImages(asset)).toEqual([
{scale: 2, src: '/project/img/logo@1.5x.png'},
]);
});

test('clamps scales larger than 3x to the 3x slot', () => {
const asset = makeAsset([4]);
expect(getCatalogImages(asset)).toEqual([
{scale: 3, src: '/project/img/logo@4x.png'},
]);
});

test('uses the largest fractional variant when several exist', () => {
const asset = makeAsset([1.5, 2.5]);
expect(getCatalogImages(asset)).toEqual([
{scale: 3, src: '/project/img/logo@2.5x.png'},
]);
});
});

describe('getImageSet', () => {
test('builds imageset path and per-scale file entries', () => {
const asset = makeAsset([1, 2, 3]);
const imageSet = getImageSet('/catalog', asset);
expect(imageSet.basePath).toBe(path.join('/catalog', 'img_logo.imageset'));
expect(imageSet.files).toEqual([
{name: 'img_logo.png', scale: 1, src: '/project/img/logo.png'},
{name: 'img_logo@2x.png', scale: 2, src: '/project/img/logo@2x.png'},
{name: 'img_logo@3x.png', scale: 3, src: '/project/img/logo@3x.png'},
]);
});

test('names the fallback rendition after its catalog slot', () => {
const asset = makeAsset([1.5]);
const imageSet = getImageSet('/catalog', asset);
expect(imageSet.files).toEqual([
{name: 'img_logo@2x.png', scale: 2, src: '/project/img/logo@1.5x.png'},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,57 @@ type ImageSet = {
files: {name: string, src: string, scale: number}[],
};

export function getImageSet(
catalogDir: string,
asset: AssetData,
scales: ReadonlyArray<number>,
): ImageSet {
// Scales an iOS asset catalog imageset can hold. actool silently drops
// renditions at any other scale (e.g. a fractional @1.5x).
const CATALOG_SCALES = [1, 2, 3];

type CatalogImage = {scale: number, src: string};

/**
* Pairs each catalog-valid scale of the asset with its source file.
*
* If the asset has no valid scale at all (e.g. only a fractional @1.5x
* variant), its closest variant is mapped into the nearest valid slot,
* mirroring the "closest larger" fallback filterPlatformAssetScales applies
* to loose files, so the imageset always contains at least one rendition
* actool will compile.
*/
export function getCatalogImages(asset: AssetData): Array<CatalogImage> {
const images: Array<CatalogImage> = [];
asset.scales.forEach((scale, idx) => {
if (CATALOG_SCALES.includes(scale)) {
images.push({scale, src: asset.files[idx]});
}
});
if (images.length === 0 && asset.scales.length > 0) {
const maxCatalogScale = CATALOG_SCALES[CATALOG_SCALES.length - 1];
let idx = asset.scales.findIndex(scale => scale > maxCatalogScale);
if (idx === -1) {
idx = asset.scales.length - 1;
}
const scale = Math.min(
maxCatalogScale,
Math.max(1, Math.ceil(asset.scales[idx])),
);
console.warn(
`warning: Asset "${asset.name}" has no 1x/2x/3x variant; ` +
`using its @${asset.scales[idx]}x file as the ${scale}x catalog rendition.`,
);
images.push({scale, src: asset.files[idx]});
}
return images;
}

export function getImageSet(catalogDir: string, asset: AssetData): ImageSet {
const fileName = getAndroidResourceIdentifier(asset);
return {
basePath: path.join(catalogDir, `${fileName}.imageset`),
files: scales.map((scale, idx) => {
files: getCatalogImages(asset).map(({scale, src}) => {
const suffix = scale === 1 ? '' : `@${scale}x`;
return {
name: `${fileName + suffix}.${asset.type}`,
scale,
src: asset.files[idx],
src,
};
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,7 @@ async function saveAssets(
cleanAssetCatalog(catalogDir);
for (const asset of assets) {
if (isCatalogAsset(asset)) {
const imageSet = getImageSet(
catalogDir,
asset,
filterPlatformAssetScales(platform, asset.scales),
);
writeImageSet(imageSet);
writeImageSet(getImageSet(catalogDir, asset));
} else {
addAssetToCopy(asset);
}
Expand Down
Loading