From 9959eee74fd255fe2afcb456192e9a84ff1484c8 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Sun, 7 Apr 2024 14:06:04 +0200 Subject: [PATCH] Fix for TIFFs with multiple image elements (#89) --- .../src/main/kotlin/Main.kt | 13 +--- .../kim/format/tiff/JpegImageDataElement.kt | 30 --------- .../ashampoo/kim/format/tiff/TiffContents.kt | 2 +- .../ashampoo/kim/format/tiff/TiffDirectory.kt | 35 ++++++---- .../kim/format/tiff/TiffImageDataElement.kt | 30 --------- .../ashampoo/kim/format/tiff/TiffReader.kt | 61 +++++++++++------- .../format/tiff/write/TiffOutputDirectory.kt | 50 +++++++++----- .../kim/format/tiff/write/TiffOutputSet.kt | 10 +-- .../format/tiff/write/TiffWriterLossless.kt | 8 +-- .../kim/format/tiff/GeoTiffUpdateTest.kt | 5 +- .../com/ashampoo/kim/updates_tif/empty.tif | Bin 40262 -> 40236 bytes .../com/ashampoo/kim/updates_tif/geotiff.tif | Bin 40404 -> 40348 bytes 12 files changed, 103 insertions(+), 141 deletions(-) delete mode 100644 src/commonMain/kotlin/com/ashampoo/kim/format/tiff/JpegImageDataElement.kt delete mode 100644 src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffImageDataElement.kt diff --git a/examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt b/examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt index 46bb2d6e..af9dda77 100644 --- a/examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt +++ b/examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt @@ -1,5 +1,4 @@ import com.ashampoo.kim.Kim -import com.ashampoo.kim.common.ByteOrder import com.ashampoo.kim.common.writeBytes import com.ashampoo.kim.format.jpeg.JpegRewriter import com.ashampoo.kim.format.tiff.TiffContents @@ -156,9 +155,7 @@ fun setGeoTiffToTiff() { OutputStreamByteWriter(outputFile.outputStream()).use { outputStreamByteWriter -> - val tiffWriter = TiffWriterLossy( - ByteOrder.LITTLE_ENDIAN - ) + val tiffWriter = TiffWriterLossy(outputSet.byteOrder) tiffWriter.write( byteWriter = outputStreamByteWriter, @@ -202,9 +199,7 @@ fun setGeoTiffToTiffUsingKotlinx() { val byteArrayByteWriter = ByteArrayByteWriter() - val tiffWriter = TiffWriterLossy( - ByteOrder.LITTLE_ENDIAN - ) + val tiffWriter = TiffWriterLossy(outputSet.byteOrder) tiffWriter.write( byteWriter = byteArrayByteWriter, @@ -261,9 +256,7 @@ fun setGeoTiffToTiffUsingKotlinxAndTiffReader() { val byteArrayByteWriter = ByteArrayByteWriter() - val tiffWriter = TiffWriterLossy( - ByteOrder.LITTLE_ENDIAN - ) + val tiffWriter = TiffWriterLossy(outputSet.byteOrder) tiffWriter.write( byteWriter = byteArrayByteWriter, diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/JpegImageDataElement.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/JpegImageDataElement.kt deleted file mode 100644 index f0e691c5..00000000 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/JpegImageDataElement.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2024 Ashampoo GmbH & Co. KG - * Copyright 2007-2023 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.ashampoo.kim.format.tiff - -class JpegImageDataElement( - offset: Int, - length: Int, - val bytes: ByteArray -) : ImageDataElement( - offset, - length -) { - - override fun toString(): String = "JpegImageData offet=$offset, length=$length" - -} diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffContents.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffContents.kt index 0e99226a..58f81255 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffContents.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffContents.kt @@ -37,7 +37,7 @@ data class TiffContents( fun getExifThumbnailBytes(): ByteArray? = directories.asSequence() - .mapNotNull { it.thumbnailImageDataElement?.bytes } + .mapNotNull { it.thumbnailBytes } .firstOrNull() fun createOutputSet(): TiffOutputSet { diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffDirectory.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffDirectory.kt index b2090204..56ed2ba0 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffDirectory.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffDirectory.kt @@ -53,8 +53,8 @@ class TiffDirectory( TiffConstants.TIFF_ENTRY_LENGTH + TiffConstants.TIFF_DIRECTORY_FOOTER_LENGTH ) { - var thumbnailImageDataElement: JpegImageDataElement? = null - var tiffImageDataElement: TiffImageDataElement? = null + var thumbnailBytes: ByteArray? = null + var tiffImageBytes: ByteArray? = null fun getDirectoryEntries(): List = entries @@ -124,7 +124,7 @@ class TiffDirectory( return field.valueBytes.toInts(field.byteOrder) } - fun getJpegImageDataElement(): ImageDataElement { + fun getJpegImageDataElement(): ImageDataElement? { val jpegInterchangeFormat = findField(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT) val jpegInterchangeFormatLength = findField(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH) @@ -137,23 +137,35 @@ class TiffDirectory( return ImageDataElement(offset, byteCount) } - throw ImageReadException("Couldn't find image data.") + return null } - fun getStripImageDataElement(): ImageDataElement { + /** + * Returns a list as tiff image bytes can be splitted upon the whole file. + * ImageIO creates small splits while GIMP creates a single big chunk. + */ + fun getStripImageDataElements(): List? { val offsetField = findField(TiffTag.TIFF_TAG_STRIP_OFFSETS) val lengthField = findField(TiffTag.TIFF_TAG_STRIP_BYTE_COUNTS) if (offsetField != null && lengthField != null) { - val offset = offsetField.toInt() - val length = lengthField.toInt() + val offsets = offsetField.toIntArray() + val lengths = lengthField.toIntArray() + + if (offsets.size != lengths.size) + throw ImageReadException("Offsets & Lengths mismatch: ${offsets.size} != ${lengths.size}") + + val imageDataElements = mutableListOf() - return ImageDataElement(offset, length) + for (index in offsets.indices) + imageDataElements.add(ImageDataElement(offsets[index], lengths[index])) + + return imageDataElements } - throw ImageReadException("Couldn't find image data.") + return null } fun createOutputDirectory(byteOrder: ByteOrder): TiffOutputDirectory { @@ -229,9 +241,8 @@ class TiffDirectory( ) } - outputDirectory.setThumbnailImageDataElement(thumbnailImageDataElement) - - outputDirectory.setTiffImageDataElement(tiffImageDataElement) + outputDirectory.setThumbnailBytes(thumbnailBytes) + outputDirectory.setTiffImageBytes(tiffImageBytes) return outputDirectory diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffImageDataElement.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffImageDataElement.kt deleted file mode 100644 index 3fd03ae1..00000000 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffImageDataElement.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2024 Ashampoo GmbH & Co. KG - * Copyright 2007-2023 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.ashampoo.kim.format.tiff - -class TiffImageDataElement( - offset: Int, - length: Int, - val bytes: ByteArray -) : ImageDataElement( - offset, - length -) { - - override fun toString(): String = "StripImageData offet=$offset, length=$length" - -} diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt index 9c89eabf..a5df6f51 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt @@ -37,6 +37,7 @@ import com.ashampoo.kim.format.tiff.taginfo.TagInfoLongs import com.ashampoo.kim.input.ByteArrayByteReader import com.ashampoo.kim.input.ByteReader import com.ashampoo.kim.input.RandomAccessByteReader +import com.ashampoo.kim.output.ByteArrayByteWriter object TiffReader { @@ -183,10 +184,10 @@ object TiffReader { ) if (directory.hasJpegImageData()) - directory.thumbnailImageDataElement = getJpegImageDataElement(byteReader, directory) + directory.thumbnailBytes = readThumbnailBytes(byteReader, directory) if (directory.hasStripImageData()) - directory.tiffImageDataElement = getStripImageDataElement(byteReader, directory) + directory.tiffImageBytes = readTiffImageBytes(byteReader, directory) addDirectory(directory) @@ -359,12 +360,12 @@ object TiffReader { * * Discarding corrupt thumbnails is not a big issue, so no exceptions will be thrown here. */ - private fun getJpegImageDataElement( + private fun readThumbnailBytes( byteReader: RandomAccessByteReader, directory: TiffDirectory - ): JpegImageDataElement? { + ): ByteArray? { - val element = directory.getJpegImageDataElement() + val element = directory.getJpegImageDataElement() ?: return null val offset = element.offset var length = element.length @@ -401,37 +402,47 @@ object TiffReader { * there are some random bytes present. */ - return JpegImageDataElement(offset, length, bytes) + return bytes } - private fun getStripImageDataElement( + private fun readTiffImageBytes( byteReader: RandomAccessByteReader, directory: TiffDirectory - ): TiffImageDataElement? { + ): ByteArray? { - val element = directory.getStripImageDataElement() + val elements = directory.getStripImageDataElements() ?: return null - val offset = element.offset - var length = element.length + val byteArrayByteWriter = ByteArrayByteWriter() - /* - * If the length is not correct (going beyond the file size) we need to adjust it. - */ - if (offset + length > byteReader.contentLength) - length = (byteReader.contentLength - offset).toInt() + for (element in elements) { - /* - * If the new length is 0 or negative, ignore this element. - */ - if (length <= 0) - return null + val offset = element.offset + var length = element.length - val bytes = byteReader.readBytes(offset.toInt(), length) + /* + * If the length is not correct (going beyond the file size) we need to adjust it. + */ + if (offset + length > byteReader.contentLength) + length = (byteReader.contentLength - offset).toInt() - if (bytes.size != length) - return null + /* + * If the new length is 0 or negative, ignore this element. + */ + if (length <= 0) + continue + + val bytes = byteReader.readBytes(offset.toInt(), length) + + /* + * Break if something is wrong. + */ + if (bytes.size != length) + return null + + byteArrayByteWriter.write(bytes) + } - return TiffImageDataElement(offset, length, bytes) + return byteArrayByteWriter.toByteArray() } /** diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputDirectory.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputDirectory.kt index dc4a281f..d1d019f2 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputDirectory.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputDirectory.kt @@ -21,9 +21,7 @@ import com.ashampoo.kim.common.ImageWriteException import com.ashampoo.kim.common.RationalNumber import com.ashampoo.kim.common.RationalNumbers import com.ashampoo.kim.common.toBytes -import com.ashampoo.kim.format.tiff.JpegImageDataElement import com.ashampoo.kim.format.tiff.TiffDirectory.Companion.description -import com.ashampoo.kim.format.tiff.TiffImageDataElement import com.ashampoo.kim.format.tiff.constant.TiffConstants.TIFF_DIRECTORY_FOOTER_LENGTH import com.ashampoo.kim.format.tiff.constant.TiffConstants.TIFF_DIRECTORY_HEADER_LENGTH import com.ashampoo.kim.format.tiff.constant.TiffConstants.TIFF_ENTRY_LENGTH @@ -78,10 +76,10 @@ class TiffOutputDirectory( override var offset: Int = UNDEFINED_VALUE - var thumbnailImageDataElement: JpegImageDataElement? = null + var thumbnailBytes: ByteArray? = null private set - var tiffImageDataElement: TiffImageDataElement? = null + var tiffImageBytes: ByteArray? = null private set fun setNextDirectory(nextDirectory: TiffOutputDirectory?) { @@ -451,12 +449,12 @@ class TiffOutputDirectory( } /* Internal, because callers should use setThumbnailBytes() */ - internal fun setThumbnailImageDataElement(thumbnailImageDataElement: JpegImageDataElement?) { - this.thumbnailImageDataElement = thumbnailImageDataElement + internal fun setThumbnailBytes(thumbnailBytes: ByteArray?) { + this.thumbnailBytes = thumbnailBytes } - internal fun setTiffImageDataElement(tiffImageDataElement: TiffImageDataElement?) { - this.tiffImageDataElement = tiffImageDataElement + internal fun setTiffImageBytes(tiffImageBytes: ByteArray?) { + this.tiffImageBytes = tiffImageBytes } override fun getItemLength(): Int = @@ -473,11 +471,12 @@ class TiffOutputDirectory( removeFieldIfPresent(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT) removeFieldIfPresent(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH) removeFieldIfPresent(TiffTag.TIFF_TAG_STRIP_OFFSETS) + removeFieldIfPresent(TiffTag.TIFF_TAG_ROWS_PER_STRIP) removeFieldIfPresent(TiffTag.TIFF_TAG_STRIP_BYTE_COUNTS) var thumbnailOffsetField: TiffOutputField? = null - if (thumbnailImageDataElement != null) { + if (thumbnailBytes != null) { thumbnailOffsetField = TiffOutputField( TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT.tag, @@ -487,7 +486,10 @@ class TiffOutputDirectory( add(thumbnailOffsetField) - val lengthValue = FieldTypeLong.writeData(thumbnailImageDataElement!!.length, outputSummary.byteOrder) + val lengthValue = FieldTypeLong.writeData( + thumbnailBytes!!.size, + outputSummary.byteOrder + ) val jpegLengthField = TiffOutputField( TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH.tag, @@ -499,7 +501,7 @@ class TiffOutputDirectory( var stripOffsetField: TiffOutputField? = null - if (tiffImageDataElement != null) { + if (tiffImageBytes != null) { stripOffsetField = TiffOutputField( TiffTag.TIFF_TAG_STRIP_OFFSETS.tag, @@ -509,7 +511,10 @@ class TiffOutputDirectory( add(stripOffsetField) - val lengthValue = FieldTypeLong.writeData(tiffImageDataElement!!.length, outputSummary.byteOrder) + val lengthValue = FieldTypeLong.writeData( + tiffImageBytes!!.size, + outputSummary.byteOrder + ) val stripByteCountsField = TiffOutputField( TiffTag.TIFF_TAG_STRIP_BYTE_COUNTS.tag, @@ -517,6 +522,19 @@ class TiffOutputDirectory( ) add(stripByteCountsField) + + /* Set to MAX value. We combine all strips into one block. */ + val rowsPerStripValue = FieldTypeLong.writeData( + Int.MAX_VALUE, + outputSummary.byteOrder + ) + + val rowsPerStripField = TiffOutputField( + TiffTag.TIFF_TAG_ROWS_PER_STRIP.tag, + FieldTypeLong, 1, rowsPerStripValue + ) + + add(rowsPerStripField) } removeFieldIfPresent(TiffTag.TIFF_TAG_TILE_OFFSETS) @@ -537,11 +555,11 @@ class TiffOutputDirectory( result.add(item) } - if (thumbnailImageDataElement != null) { + if (thumbnailBytes != null) { val item: TiffOutputItem = TiffOutputValue( "thumbnailImageDataElement", - thumbnailImageDataElement!!.bytes + thumbnailBytes!! ) result.add(item) @@ -549,11 +567,11 @@ class TiffOutputDirectory( outputSummary.addOffsetItem(TiffOffsetItem(item, thumbnailOffsetField!!)) } - if (tiffImageDataElement != null) { + if (tiffImageBytes != null) { val item: TiffOutputItem = TiffOutputValue( "tiffImageDataElement", - tiffImageDataElement!!.bytes + tiffImageBytes!! ) result.add(item) diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputSet.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputSet.kt index 2ee50546..d9b5ed7d 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputSet.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputSet.kt @@ -23,7 +23,6 @@ import com.ashampoo.kim.common.ImageWriteException import com.ashampoo.kim.common.RationalNumber.Companion.valueOf import com.ashampoo.kim.common.RationalNumbers import com.ashampoo.kim.common.toExifDateString -import com.ashampoo.kim.format.tiff.JpegImageDataElement import com.ashampoo.kim.format.tiff.constant.ExifTag import com.ashampoo.kim.format.tiff.constant.GpsTag import com.ashampoo.kim.format.tiff.constant.TiffConstants @@ -145,14 +144,7 @@ class TiffOutputSet( val thumbnailDirectory = getOrCreateThumbnailDirectory() - thumbnailDirectory.setThumbnailImageDataElement( - JpegImageDataElement( - /* Offset will be calculated, but the block should come early in the file. */ - offset = -1, - length = thumbnailBytes.size, - bytes = thumbnailBytes - ) - ) + thumbnailDirectory.setThumbnailBytes(thumbnailBytes) } /** diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffWriterLossless.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffWriterLossless.kt index c6fe4b08..71bab4c5 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffWriterLossless.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffWriterLossless.kt @@ -65,7 +65,7 @@ class TiffWriterLossless( * Don't write IFD1 directories without image data. If the thumbnail image is broken * on load, we should drop the IFD1 on rewrite entirely. It's just a waste of space. */ - if (directory.type == 1 && directory.thumbnailImageDataElement == null) + if (directory.type == 1 && directory.thumbnailBytes == null) continue elements.add(directory) @@ -87,12 +87,12 @@ class TiffWriterLossless( } } - directory.thumbnailImageDataElement?.let { + directory.getJpegImageDataElement()?.let { elements.add(it) } - directory.tiffImageDataElement?.let { - elements.add(it) + directory.getStripImageDataElements()?.let { + elements.addAll(it) } } diff --git a/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt b/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt index bdd73a5a..685d0052 100644 --- a/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt +++ b/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt @@ -16,7 +16,6 @@ package com.ashampoo.kim.format.tiff import com.ashampoo.kim.Kim -import com.ashampoo.kim.common.ByteOrder import com.ashampoo.kim.common.readBytes import com.ashampoo.kim.common.writeBytes import com.ashampoo.kim.format.tiff.constant.GeoTiffTag @@ -66,9 +65,7 @@ class GeoTiffUpdateTest { val byteWriter = ByteArrayByteWriter() - val writer = TiffWriterLossy( - ByteOrder.LITTLE_ENDIAN - ) + val writer = TiffWriterLossy(outputSet.byteOrder) writer.write( byteWriter = byteWriter, diff --git a/src/commonTest/resources/com/ashampoo/kim/updates_tif/empty.tif b/src/commonTest/resources/com/ashampoo/kim/updates_tif/empty.tif index 32265cc80f13e8f12915a099db5c455b9a354601..168a3beaf3312d9a3795dde6d89c59c2a2ab08a6 100644 GIT binary patch delta 256 zcmX@Mi)qa+rg~pr1}z2#1`Y;ZMg|6EAd8V91xPR=v6;YZ7NFc3MrNouP%$GLl+6TW z3o?M@S%G*9qbO7yD8eWPWs3mW;&3%57^T2!85wdJ8167iLpcl#PZ%|jEC@odfd+#B x2NW_eykTHa&|zS(&tYJQTf@Lm3*^f?U}B(mfHuSMMu9cP6BRf%?=kMl003($79jut delta 286 zcmZ3pi|N=dCJs+eEe4M{3=EqCa_$&xPRO~zw0VI^Pe#2U0|O&710w?iLkbWxBC(l( zY!;xH)?5Y#W~eww9UGL*1Y`>`vViq+0NFxFYDA%IkbW^Jy8*}+hpKk~xYo`i%RoS7y#5#8Z7_- diff --git a/src/commonTest/resources/com/ashampoo/kim/updates_tif/geotiff.tif b/src/commonTest/resources/com/ashampoo/kim/updates_tif/geotiff.tif index c97ae5926d96acdfb523759c3ca6d51a21d50637..940b29665c78fd85fc6ced4d915418b06ad670e1 100644 GIT binary patch delta 348 zcmcbzn`zE&rg~pr1}z2#1`Y;6Mg|6EAd8V91xPR=v6;YZ7NFc6MrNouP%$GLl+6TW z3o?M@L838?LP%;vp=_WmqZmX@{r~^}8O5RM>lx-aFiJ77LNqfxVU&im-!N(*85D$I zH}f&@0M&yW`=zA`%w_``&(pphEG7VC+W_T2fCCDFLO>oI+v^!FU$LV+VZSol$5p;F ztlB0!GD=SlU}Uv$P`q-L$>)TtgGX6__q^YNU{gQ@NG~uTK)wXBnK;0-z%v$znM|7( RFs?A3yn&HtvxjL<1^|TjDc}GA delta 413 zcmbQUo9W7Krg~3LEd~w-1_luZ21aHEMj$H%h#8UCOh7gZPz)r=3>62dV}r7pfNViV z7O>taKnWovHKI^9NWU1A-2h~ZL)AOf&tYJYVq^tt<^XDzhOz~KY#AhjWEq(lcz_xu zfZ`fZagfD9NNR$CY>-7NKs9{LJPgc0whoZp)WXBS24uSc+3VYx83cgr5+EA{>==N) zhd_{0kSL7y$jmEAElMvc%}W7_LtsMrjup!d_3YcMX85l9sJwr&AfxbP1x6Mawq$TU z;lp(GilT$y?|I$FsS)f2?WOyb33