diff --git a/CHANGELOG.md b/CHANGELOG.md index 71011bf79..e75d2d440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Fixed + +- crash when loading SVG defined with large dimensions + ## [v1.10.1] - 2023-12-21 - Cataloguing: detect/filter `Ultra HDR` diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchBytesHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchBytesHandler.kt index 2c96888b6..232fa09cd 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchBytesHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaFetchBytesHandler.kt @@ -89,6 +89,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler { MimeTypes.SVG -> SvgRegionFetcher(context).fetch( uri = uri, sizeBytes = sizeBytes, + scale = sampleSize, regionRect = regionRect, imageWidth = imageWidth, imageHeight = imageHeight, diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt index e453c7ffc..882844c6e 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt @@ -25,6 +25,7 @@ class SvgRegionFetcher internal constructor( suspend fun fetch( uri: Uri, sizeBytes: Long?, + scale: Int, regionRect: Rect, imageWidth: Int, imageHeight: Int, @@ -36,14 +37,6 @@ class SvgRegionFetcher internal constructor( return } - // use `Long` as rect size could be unexpectedly large and go beyond `Int` max - val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * regionRect.width() * regionRect.height() - if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) { - // decoding a region that large would yield an OOM when creating the bitmap - result.error("fetch-read-large-region", "SVG region too large for uri=$uri regionRect=$regionRect", null) - return - } - var currentSvgRef = lastSvgRef if (currentSvgRef != null && currentSvgRef.uri != uri) { currentSvgRef = null @@ -75,8 +68,8 @@ class SvgRegionFetcher internal constructor( val viewBox = svg.documentViewBox val svgWidth = viewBox.width() val svgHeight = viewBox.height() - val xf = imageWidth / ceil(svgWidth) - val yf = imageHeight / ceil(svgHeight) + val xf = imageWidth / scale / ceil(svgWidth) + val yf = imageHeight / scale / ceil(svgHeight) // some SVG paths do not respect the rendering viewbox and do not reach its edges // so we render to a slightly larger bitmap, using a slightly larger viewbox, // and crop that bitmap to the target region size @@ -95,6 +88,15 @@ class SvgRegionFetcher internal constructor( val targetBitmapWidth = regionRect.width() val targetBitmapHeight = regionRect.height() + + // use `Long` as rect size could be unexpectedly large and go beyond `Int` max + val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * targetBitmapWidth * targetBitmapHeight + if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) { + // decoding a region that large would yield an OOM when creating the bitmap + result.error("fetch-read-large-region", "SVG region too large for uri=$uri regionRect=$regionRect", null) + return + } + var bitmap = Bitmap.createBitmap( targetBitmapWidth + bleedX * 2, targetBitmapHeight + bleedY * 2, diff --git a/lib/utils/math_utils.dart b/lib/utils/math_utils.dart index 82be6858d..68a3f1ec9 100644 --- a/lib/utils/math_utils.dart +++ b/lib/utils/math_utils.dart @@ -3,7 +3,9 @@ import 'dart:ui'; int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / ln2).floor()).toInt(); -int smallestPowerOf2(num x) => x < 1 ? 1 : pow(2, (log(x) / ln2).ceil()).toInt(); +num smallestPowerOf2(num x, {bool allowNegativePower = false}) { + return x < 1 && !allowNegativePower ? 1 : pow(2, (log(x) / ln2).ceil()); +} double roundToPrecision(final double value, {required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals); diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 2f4cea000..d59476297 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -439,7 +439,7 @@ class _GeoMapState extends State { if (points.length != geoEntry.pointsSize) { // `Fluster.points()` method does not always return all the points contained in a cluster // the higher `nodeSize` is, the higher the chance to get all the points (i.e. as many as the cluster `pointsSize`) - _slowMarkerCluster ??= _buildFluster(nodeSize: smallestPowerOf2(entries.length)); + _slowMarkerCluster ??= _buildFluster(nodeSize: smallestPowerOf2(entries.length).toInt()); points = _slowMarkerCluster!.points(clusterId); assert(points.length == geoEntry.pointsSize, 'got ${points.length}/${geoEntry.pointsSize} for geoEntry=$geoEntry'); } diff --git a/lib/widgets/viewer/visual/vector.dart b/lib/widgets/viewer/visual/vector.dart index eae983d37..b76cbb5ab 100644 --- a/lib/widgets/viewer/visual/vector.dart +++ b/lib/widgets/viewer/visual/vector.dart @@ -286,7 +286,7 @@ class _VectorImageViewState extends State { required double scale, required double devicePixelRatio, }) => - smallestPowerOf2(scale * devicePixelRatio).toDouble(); + smallestPowerOf2(scale * devicePixelRatio, allowNegativePower: true).toDouble(); } typedef _BackgroundFrameBuilder = Widget Function(Widget child, int? frame, Rect tileRect); diff --git a/test/utils/math_utils_test.dart b/test/utils/math_utils_test.dart index 0d067bcd6..7d5989d42 100644 --- a/test/utils/math_utils_test.dart +++ b/test/utils/math_utils_test.dart @@ -20,6 +20,8 @@ void main() { expect(smallestPowerOf2(-42), 1); expect(smallestPowerOf2(.5), 1); expect(smallestPowerOf2(1.5), 2); + expect(smallestPowerOf2(0.5, allowNegativePower: true), 0.5); + expect(smallestPowerOf2(0.1, allowNegativePower: true), 0.125); }); test('rounding to a given precision after the decimal', () {