Problem
MarkerImage.scale is public API — it is in the Nitro spec (package/src/native/specs/overlays.ts:7),
re-exported from package/src/types/index.ts, documented in the README, and used by the example
app — but no provider ever reads it when sizing or decoding an image.
It is referenced in exactly four native places, and all four are cache keys or change-detection
hashes:
| File |
Line |
Use |
package/ios/MarkerImageLoader.swift |
47-48 |
image cache key |
package/ios/MarkerDescriptor+Fingerprint.swift |
14 |
displayed-identity hash |
package/android/.../MarkerIconFactory.kt |
166 |
icon cache key |
package/android/.../MarkerDescriptor+DisplayedIdentity.kt |
16 |
displayed-identity hash |
The two functions that actually decide how big a marker image is drawn both bail out when
width/height are missing and never look at scale:
// package/ios/MarkerImageLoader.swift:103
private static func resize(_ uiImage: UIImage, image: MarkerImage) -> UIImage {
guard let width = image.width, let height = image.height else {
return uiImage // scale is not consulted here or anywhere below
}
// package/android/.../MarkerIconFactory.kt:509
private fun resizeBitmap(source: Bitmap, image: MarkerImage): Bitmap {
val width = image.width ?: return source
val height = image.height ?: return source
So width/height are the only working sizing path; scale is inert.
Reproduction
<Marker
coordinate={{ latitude: 52.2297, longitude: 21.0122 }}
image={{
// 50×82 px bitmap authored for a 25×41 dp pin
uri: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
scale: 2,
}}
/>
Expected: a 25×41 dp pin on both platforms.
Actual:
- iOS (MapKit and Google alike) —
UIImage(data:) produces an image with scale = 1.0, and
resize returns early, so the pin draws at 50×82 pt — 2× too large, on every device.
- Android — the decode runs with
inScaled = false (MarkerIconFactory.kt:356) and
resizeBitmap returns early, so the 50×82 px bitmap maps 1:1 onto physical pixels: 25×41 dp on
an xhdpi (2×) phone, 16.7×27.3 dp on xxhdpi (3×), 50×82 dp on mdpi.
On a 3× phone the same descriptor therefore renders roughly 3× larger on iOS than on Android.
Neither platform honours the size the author asked for, and the Android result additionally depends
on the device's density.
Second symptom on Android: with width/height absent, displaySizePx returns null
(MarkerIconFactory.kt:148-161), applyAnchor falls back to a 0×0 size (:66-71), and
effectiveGoogleMapsAnchor short-circuits on a zero size
(MarkerDescriptor+MarkerOptions.kt:20-22) — so centerOffset is silently dropped for those
markers too.
Why this survived
- Every existing
scale test asserts pass-through, never rendering:
resolveMarkerImage.test.ts:103-148 checks the object is returned unchanged and cached;
descriptorEquality.test.ts:118-121 checks that a changed scale invalidates the descriptor.
Both pass against a field that does nothing.
- The
require() path masks it. Image.resolveAssetSource always returns width/height in dp,
so bundled assets size correctly and scale is redundant there. Only an explicitly constructed
MarkerImage depends on it — i.e. the remote-URL case, where React Native cannot know the size.
- The example app passes all three together
(example/examples/customMarkerImages.ts:13-18: { width: 25, height: 41, scale: 2 }), so the
Custom markers scenario looks correct and hides the gap.
The field has never worked: it landed with the original marker-image feature (#25, 9a7cab6) and
was only ever wired into the cache key.
Suggested fix
Implement it: display size = decoded pixel size ÷ scale, with an explicit width/height still
winning.
- iOS — decode at the requested scale rather than resizing afterwards:
UIImage(data: data, scale: CGFloat(image.scale ?? 1)) in loadRemote, and the equivalent for
the file:// / path branches of loadLocal. UIImage.size is then already in points, so
NitroImageAnnotationView (:67), GMSMarker.icon (GoogleMarkerVisualApplier.swift:119) and
anchorImageSize (:137-149) all start reporting the right size with no further change.
- Android — when
width/height are absent and scale is present, target px =
source px × density ÷ scale in resizeBitmap (:509-523), and displaySizePx (:148-161) has
to return that same size so the anchor/centerOffset math stops degenerating.
- The generated struct already carries the field, so this is a native-only change — no spec edit
and no nitrogen regen.
Both sizing helpers are private today, so the arithmetic wants its own file under the repo's
existing Type+operation convention — MarkerImage+displaySize.swift and
MarkerImage+displaySize.kt — unit-tested in package/iosTests and package/android/src/test
alongside the existing MarkerDisplayedIdentityTest.
The alternative is to delete scale from the spec and the README. That is a breaking change to a
public type and it removes the only way to size a remote image whose intrinsic size the app does not
know, so I would not choose it.
Either way README.md:358 needs updating — it currently offers width / height / scale as
interchangeable knobs:
Retina assets: pass require() and let Metro resolve @2x/@3x; optional explicit
width/height/scale on MarkerImage.
Observed on 1.2.1 (ccbc2a8), both providers, iOS and Android.
Problem
MarkerImage.scaleis public API — it is in the Nitro spec (package/src/native/specs/overlays.ts:7),re-exported from
package/src/types/index.ts, documented in the README, and used by the exampleapp — but no provider ever reads it when sizing or decoding an image.
It is referenced in exactly four native places, and all four are cache keys or change-detection
hashes:
package/ios/MarkerImageLoader.swiftpackage/ios/MarkerDescriptor+Fingerprint.swiftpackage/android/.../MarkerIconFactory.ktpackage/android/.../MarkerDescriptor+DisplayedIdentity.ktThe two functions that actually decide how big a marker image is drawn both bail out when
width/heightare missing and never look atscale:So
width/heightare the only working sizing path;scaleis inert.Reproduction
Expected: a 25×41 dp pin on both platforms.
Actual:
UIImage(data:)produces an image withscale = 1.0, andresizereturns early, so the pin draws at 50×82 pt — 2× too large, on every device.inScaled = false(MarkerIconFactory.kt:356) andresizeBitmapreturns early, so the 50×82 px bitmap maps 1:1 onto physical pixels: 25×41 dp onan xhdpi (2×) phone, 16.7×27.3 dp on xxhdpi (3×), 50×82 dp on mdpi.
On a 3× phone the same descriptor therefore renders roughly 3× larger on iOS than on Android.
Neither platform honours the size the author asked for, and the Android result additionally depends
on the device's density.
Second symptom on Android: with
width/heightabsent,displaySizePxreturnsnull(
MarkerIconFactory.kt:148-161),applyAnchorfalls back to a 0×0 size (:66-71), andeffectiveGoogleMapsAnchorshort-circuits on a zero size(
MarkerDescriptor+MarkerOptions.kt:20-22) — socenterOffsetis silently dropped for thosemarkers too.
Why this survived
scaletest asserts pass-through, never rendering:resolveMarkerImage.test.ts:103-148checks the object is returned unchanged and cached;descriptorEquality.test.ts:118-121checks that a changedscaleinvalidates the descriptor.Both pass against a field that does nothing.
require()path masks it.Image.resolveAssetSourcealways returnswidth/heightin dp,so bundled assets size correctly and
scaleis redundant there. Only an explicitly constructedMarkerImagedepends on it — i.e. the remote-URL case, where React Native cannot know the size.(
example/examples/customMarkerImages.ts:13-18:{ width: 25, height: 41, scale: 2 }), so theCustom markers scenario looks correct and hides the gap.
The field has never worked: it landed with the original marker-image feature (#25,
9a7cab6) andwas only ever wired into the cache key.
Suggested fix
Implement it: display size = decoded pixel size ÷
scale, with an explicitwidth/heightstillwinning.
UIImage(data: data, scale: CGFloat(image.scale ?? 1))inloadRemote, and the equivalent forthe
file:/// path branches ofloadLocal.UIImage.sizeis then already in points, soNitroImageAnnotationView(:67),GMSMarker.icon(GoogleMarkerVisualApplier.swift:119) andanchorImageSize(:137-149) all start reporting the right size with no further change.width/heightare absent andscaleis present, target px =source px × density ÷ scale in
resizeBitmap(:509-523), anddisplaySizePx(:148-161) hasto return that same size so the anchor/
centerOffsetmath stops degenerating.and no nitrogen regen.
Both sizing helpers are private today, so the arithmetic wants its own file under the repo's
existing
Type+operationconvention —MarkerImage+displaySize.swiftandMarkerImage+displaySize.kt— unit-tested inpackage/iosTestsandpackage/android/src/testalongside the existing
MarkerDisplayedIdentityTest.The alternative is to delete
scalefrom the spec and the README. That is a breaking change to apublic type and it removes the only way to size a remote image whose intrinsic size the app does not
know, so I would not choose it.
Either way
README.md:358needs updating — it currently offerswidth/height/scaleasinterchangeable knobs:
Observed on
1.2.1(ccbc2a8), both providers, iOS and Android.