#853 apply scale when decoding SVG

This commit is contained in:
Thibault Deckers 2023-12-24 16:10:26 +01:00
parent eb004d8eca
commit 8981900d6b
7 changed files with 24 additions and 13 deletions

View file

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
### Fixed
- crash when loading SVG defined with large dimensions
## <a id="v1.10.1"></a>[v1.10.1] - 2023-12-21 ## <a id="v1.10.1"></a>[v1.10.1] - 2023-12-21
- Cataloguing: detect/filter `Ultra HDR` - Cataloguing: detect/filter `Ultra HDR`

View file

@ -89,6 +89,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
MimeTypes.SVG -> SvgRegionFetcher(context).fetch( MimeTypes.SVG -> SvgRegionFetcher(context).fetch(
uri = uri, uri = uri,
sizeBytes = sizeBytes, sizeBytes = sizeBytes,
scale = sampleSize,
regionRect = regionRect, regionRect = regionRect,
imageWidth = imageWidth, imageWidth = imageWidth,
imageHeight = imageHeight, imageHeight = imageHeight,

View file

@ -25,6 +25,7 @@ class SvgRegionFetcher internal constructor(
suspend fun fetch( suspend fun fetch(
uri: Uri, uri: Uri,
sizeBytes: Long?, sizeBytes: Long?,
scale: Int,
regionRect: Rect, regionRect: Rect,
imageWidth: Int, imageWidth: Int,
imageHeight: Int, imageHeight: Int,
@ -36,14 +37,6 @@ class SvgRegionFetcher internal constructor(
return 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 var currentSvgRef = lastSvgRef
if (currentSvgRef != null && currentSvgRef.uri != uri) { if (currentSvgRef != null && currentSvgRef.uri != uri) {
currentSvgRef = null currentSvgRef = null
@ -75,8 +68,8 @@ class SvgRegionFetcher internal constructor(
val viewBox = svg.documentViewBox val viewBox = svg.documentViewBox
val svgWidth = viewBox.width() val svgWidth = viewBox.width()
val svgHeight = viewBox.height() val svgHeight = viewBox.height()
val xf = imageWidth / ceil(svgWidth) val xf = imageWidth / scale / ceil(svgWidth)
val yf = imageHeight / ceil(svgHeight) val yf = imageHeight / scale / ceil(svgHeight)
// some SVG paths do not respect the rendering viewbox and do not reach its edges // 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, // so we render to a slightly larger bitmap, using a slightly larger viewbox,
// and crop that bitmap to the target region size // and crop that bitmap to the target region size
@ -95,6 +88,15 @@ class SvgRegionFetcher internal constructor(
val targetBitmapWidth = regionRect.width() val targetBitmapWidth = regionRect.width()
val targetBitmapHeight = regionRect.height() 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( var bitmap = Bitmap.createBitmap(
targetBitmapWidth + bleedX * 2, targetBitmapWidth + bleedX * 2,
targetBitmapHeight + bleedY * 2, targetBitmapHeight + bleedY * 2,

View file

@ -3,7 +3,9 @@ import 'dart:ui';
int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / ln2).floor()).toInt(); 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); double roundToPrecision(final double value, {required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals);

View file

@ -439,7 +439,7 @@ class _GeoMapState extends State<GeoMap> {
if (points.length != geoEntry.pointsSize) { if (points.length != geoEntry.pointsSize) {
// `Fluster.points()` method does not always return all the points contained in a cluster // `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`) // 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); points = _slowMarkerCluster!.points(clusterId);
assert(points.length == geoEntry.pointsSize, 'got ${points.length}/${geoEntry.pointsSize} for geoEntry=$geoEntry'); assert(points.length == geoEntry.pointsSize, 'got ${points.length}/${geoEntry.pointsSize} for geoEntry=$geoEntry');
} }

View file

@ -286,7 +286,7 @@ class _VectorImageViewState extends State<VectorImageView> {
required double scale, required double scale,
required double devicePixelRatio, required double devicePixelRatio,
}) => }) =>
smallestPowerOf2(scale * devicePixelRatio).toDouble(); smallestPowerOf2(scale * devicePixelRatio, allowNegativePower: true).toDouble();
} }
typedef _BackgroundFrameBuilder = Widget Function(Widget child, int? frame, Rect tileRect); typedef _BackgroundFrameBuilder = Widget Function(Widget child, int? frame, Rect tileRect);

View file

@ -20,6 +20,8 @@ void main() {
expect(smallestPowerOf2(-42), 1); expect(smallestPowerOf2(-42), 1);
expect(smallestPowerOf2(.5), 1); expect(smallestPowerOf2(.5), 1);
expect(smallestPowerOf2(1.5), 2); 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', () { test('rounding to a given precision after the decimal', () {