Skip to content

Fix asset catalog scale-to-file pairing for assets with non-standard scales - #57825

Open
janicduplessis wants to merge 1 commit into
react:mainfrom
janicduplessis:fix-asset-catalog-scale-pairing
Open

Fix asset catalog scale-to-file pairing for assets with non-standard scales#57825
janicduplessis wants to merge 1 commit into
react:mainfrom
janicduplessis:fix-asset-catalog-scale-pairing

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary:

saveAssets filters an asset's scales through filterPlatformAssetScales, but getImageSet indexes the unfiltered asset.files, so an asset with scales [1, 1.5, 2, 3] gets the 1.5x file in its 2x catalog slot and the 2x file in its 3x slot: Assets.car ships the wrong images. An asset with no standard scale at all (only @1.5x, say) produces an imageset actool silently drops from the car, which makes the image unloadable when RCTUseAssetCatalog is on, since the catalog runtime has no filesystem fallback (#30129).

Fix, in assetCatalogIOS.js:

  • Each catalog slot (1x/2x/3x) is paired with its own file.
  • An asset with no valid scale maps its closest variant into the nearest valid slot, the same "closest larger" rule loose files already get, and warns in the build log. Every imageset now holds at least one rendition actool will compile.

Related: expo/expo#48525 applies the same fix to Expo CLI's mirrored implementation.

Changelog:

[IOS] [FIXED] - Asset catalog imagesets paired wrong files for assets with non-standard scales

Test Plan:

New unit tests in assetCatalogIOS-test.js: standard 1x/2x/3x pairing, mixed [1, 1.5, 2, 3] (regression for the file shift), fractional-only [1.5], and [4] clamping to the 3x slot.

yarn jest packages/community-cli-plugin/src/commands/bundle
Tests: 18 passed, 18 total

End-to-end

Bundled a test app through the real pipeline (Metro → saveAssets → actool → assetutil --info on the compiled Assets.car) with two assets: logo at scales [1, 1.5, 2, 3] (100/150/200/300 px) and star with only a @1.5x file (150 px).

Rendition in Assets.car before after
img_logo 1x 100 px 100 px
img_logo 2x 150 px (the 1.5x file) 200 px
img_logo 3x 200 px (the 2x file) 300 px
img_star absent (actool dropped the @1.5x imageset) 150 px in the 2x slot, with a build-log warning

Running on an iPhone 17 Pro simulator (3x), same app built with the buggy and fixed CLI — the labeled tiles show which file the catalog actually served, and star goes from missing to rendering:

Before After
Repro app used for the screenshots

Built private/helloworld (Release, simulator) with RCTUseAssetCatalog set to true in its Info.plist, and these assets in img/, where each file is a solid tile with its scale label and pixel size baked into the image so a screenshot shows exactly which file got served: logo.png (100px, "1x"), logo@1.5x.png (150px, "1.5x"), logo@2x.png (200px, "2x"), logo@3x.png (300px, "3x"), and star@1.5x.png (150px, "STAR") with no other variants.

// index.js
import React from 'react';
import {AppRegistry, Image, Text, View, StyleSheet} from 'react-native';

const styles = StyleSheet.create({
  root: {flex: 1, backgroundColor: '#111', alignItems: 'center', justifyContent: 'center'},
  label: {color: '#fff', fontSize: 16, marginTop: 24, marginBottom: 8, fontWeight: '600'},
  box: {width: 100, height: 100, borderWidth: 2, borderColor: '#666'},
  img: {width: 100, height: 100},
});

const App = () => (
  <View style={styles.root}>
    <Text style={styles.label}>logo.png (has 1x/1.5x/2x/3x)</Text>
    <View style={styles.box}>
      <Image style={styles.img} source={require('./img/logo.png')} />
    </View>
    <Text style={styles.label}>star.png (only @1.5x)</Text>
    <View style={styles.box}>
      <Image style={styles.img} source={require('./img/star.png')} />
    </View>
  </View>
);

AppRegistry.registerComponent('HelloWorld', () => App);

On the 3x simulator JS resolves logo to the 3x variant, so the tile that renders is the file the catalog's 3x slot actually contains: the 2x-labeled tile before the fix, the 3x tile after. star resolves to its only variant (@1.5x); before the fix its imageset is dropped by actool and the box renders empty.

…ndard scales

The iOS asset catalog path filtered asset scales through
filterPlatformAssetScales but indexed the unfiltered asset.files array,
so an asset with scales [1, 1.5, 2, 3] would associate the 2x catalog
rendition with the 1.5x file and the 3x rendition with the 2x file.

Additionally, assets with no standard scale at all (e.g. only @1.5x)
produced an imageset actool silently drops from Assets.car, making the
image unloadable when RCTUseAssetCatalog is enabled. These assets now map
their closest variant into the nearest valid slot (the same "closest
larger" fallback filterPlatformAssetScales applies to loose files) with
a build-log warning, so every imageset contains at least one rendition
actool will compile.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 5, 2026
@facebook-github-tools facebook-github-tools Bot added the Contributor A React Native contributor. label Aug 5, 2026
@janicduplessis
janicduplessis marked this pull request as ready for review August 5, 2026 12:27
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Contributor A React Native contributor. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant