diff --git a/README.md b/README.md index 80b5317b..55383a7a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ ![Android](https://img.shields.io/badge/-Android-gray.svg?style=flat) ![macOS](https://img.shields.io/badge/-macOS-gray.svg?style=flat) ![iOS](https://img.shields.io/badge/-iOS-gray.svg?style=flat) +![Windows](https://img.shields.io/badge/-Windows-gray.svg?style=flat) +![WASM](https://img.shields.io/badge/-WASM-gray.svg?style=flat) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=kim&metric=coverage)](https://sonarcloud.io/summary/new_code?id=kim) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.ashampoo/kim/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.ashampoo/kim) @@ -32,7 +34,7 @@ of Ashampoo Photos, which, in turn, is driven by user community feedback. ## Installation ``` -implementation("com.ashampoo:kim:0.7.5") +implementation("com.ashampoo:kim:0.8") ``` ## Sample usages @@ -137,7 +139,7 @@ val newBytes = Kim.updateThumbnail( ## Limitations * Inability to update EXIF, IPTC and XMP in JPG files simultaneously. -* Insufficient error handling for broken or non-standard conforming files. +* There is no implementation of WebAssembly (WASM) for ZLib compression, which means that PNG files utilizing this compression cannot be processed. ## Contributions diff --git a/build.gradle.kts b/build.gradle.kts index b1725a14..794479e9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl plugins { kotlin("multiplatform") version "1.9.21" @@ -23,7 +24,7 @@ repositories { val productName = "Ashampoo Kim" val ktorVersion: String = "2.3.7" -val xmpCoreVersion: String = "0.2.4" +val xmpCoreVersion: String = "0.3" val dateTimeVersion: String = "0.5.0" val testRessourcesVersion: String = "0.4.0" val ioCoreVersion: String = "0.3.0" @@ -144,6 +145,13 @@ kotlin { } } + @OptIn(ExperimentalWasmDsl::class) + wasmJs() + +// Note: Missing support in kotlinx-datetime +// @OptIn(ExperimentalWasmDsl::class) +// wasmWasi() + @Suppress("UnusedPrivateMember") // False positive val commonMain by sourceSets.getting { @@ -152,10 +160,6 @@ kotlin { /* Date handling */ implementation("org.jetbrains.kotlinx:kotlinx-datetime:$dateTimeVersion") - /* Needed for Charset class. */ - /* Defined as api() to prevent problems when used from a pure-java project. */ - api("io.ktor:ktor-io:$ktorVersion") - /* XMP handling */ api("com.ashampoo:xmpcore:$xmpCoreVersion") @@ -202,15 +206,31 @@ kotlin { } } - val jvmMain by sourceSets.getting + val ktorMain by sourceSets.creating { - @Suppress("UnusedPrivateMember", "UNUSED_VARIABLE") // False positive - val androidMain by sourceSets.getting { - dependsOn(jvmMain) + dependsOn(commonMain) + + dependencies { + + api("io.ktor:ktor-io:$ktorVersion") + } } val posixMain by sourceSets.creating { + dependsOn(commonMain) + dependsOn(ktorMain) + } + + val jvmMain by sourceSets.getting { + + dependsOn(commonMain) + dependsOn(ktorMain) + } + + @Suppress("UnusedPrivateMember", "UNUSED_VARIABLE") // False positive + val androidMain by sourceSets.getting { + dependsOn(jvmMain) } @Suppress("UnusedPrivateMember", "UNUSED_VARIABLE") // False positive @@ -227,6 +247,7 @@ kotlin { val appleMain by sourceSets.creating { dependsOn(commonMain) + dependsOn(ktorMain) dependsOn(posixMain) iosArm64Main.dependsOn(this) @@ -234,6 +255,17 @@ kotlin { macosX64Main.dependsOn(this) macosArm64Main.dependsOn(this) } + + val wasmJsMain by sourceSets.getting + // val wasmWasiMain by sourceSets.getting + + val wasmMain by sourceSets.creating { + + dependsOn(commonMain) + + wasmJsMain.dependsOn(this) + // wasmWasiMain.dependsOn(this) + } } // region Writing version.txt for GitHub Actions @@ -303,6 +335,8 @@ afterEvaluate { val signMacosArm64Publication by tasks.getting val signMacosX64Publication by tasks.getting val signWinPublication by tasks.getting + val signWasmJsPublication by tasks.getting + val signWasmWasiPublication by tasks.getting val signKotlinMultiplatformPublication by tasks.getting val publishJvmPublicationToSonatypeRepository by tasks.getting @@ -312,6 +346,8 @@ afterEvaluate { val publishMacosArm64PublicationToSonatypeRepository by tasks.getting val publishMacosX64PublicationToSonatypeRepository by tasks.getting val publishWinPublicationToSonatypeRepository by tasks.getting + val publishWasmJsPublicationToSonatypeRepository by tasks.getting + val publishWasmWasiPublicationToSonatypeRepository by tasks.getting val publishKotlinMultiplatformPublicationToSonatypeRepository by tasks.getting val publishAllPublicationsToSonatypeRepository by tasks.getting @@ -319,7 +355,8 @@ afterEvaluate { signJvmPublication, signAndroidReleasePublication, signIosArm64Publication, signIosSimulatorArm64Publication, signMacosArm64Publication, signMacosX64Publication, - signWinPublication, signKotlinMultiplatformPublication + signWinPublication, signWasmJsPublication, signWasmWasiPublication, + signKotlinMultiplatformPublication ) val publishTasks = listOf( @@ -330,6 +367,8 @@ afterEvaluate { publishMacosArm64PublicationToSonatypeRepository, publishMacosX64PublicationToSonatypeRepository, publishWinPublicationToSonatypeRepository, + publishWasmJsPublicationToSonatypeRepository, + publishWasmWasiPublicationToSonatypeRepository, publishKotlinMultiplatformPublicationToSonatypeRepository, publishAllPublicationsToSonatypeRepository ) diff --git a/src/commonMain/kotlin/com/ashampoo/kim/Kim.kt b/src/commonMain/kotlin/com/ashampoo/kim/Kim.kt index 8614436e..3732181d 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/Kim.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/Kim.kt @@ -37,16 +37,12 @@ import com.ashampoo.kim.input.ByteArrayByteReader import com.ashampoo.kim.input.ByteReader import com.ashampoo.kim.input.DefaultRandomAccessByteReader import com.ashampoo.kim.input.KotlinIoSourceByteReader -import com.ashampoo.kim.input.KtorByteReadChannelByteReader -import com.ashampoo.kim.input.KtorInputByteReader import com.ashampoo.kim.input.PrePendingByteReader +import com.ashampoo.kim.input.use import com.ashampoo.kim.model.ImageFormat import com.ashampoo.kim.model.MetadataUpdate import com.ashampoo.kim.output.ByteArrayByteWriter import com.ashampoo.kim.output.ByteWriter -import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.core.ByteReadPacket -import io.ktor.utils.io.core.use import kotlinx.io.files.Path object Kim { @@ -71,16 +67,6 @@ object Kim { } } - @kotlin.jvm.JvmStatic - @Throws(ImageReadException::class) - fun readMetadata(byteReadPacket: ByteReadPacket): ImageMetadata? = - readMetadata(KtorInputByteReader(byteReadPacket)) - - @kotlin.jvm.JvmStatic - @Throws(ImageReadException::class) - fun readMetadata(byteReadChannel: ByteReadChannel, contentLength: Long): ImageMetadata? = - readMetadata(KtorByteReadChannelByteReader(byteReadChannel, contentLength)) - @kotlin.jvm.JvmStatic @Throws(ImageReadException::class) fun readMetadata( diff --git a/src/commonMain/kotlin/com/ashampoo/kim/common/ByteArrayExtensions.kt b/src/commonMain/kotlin/com/ashampoo/kim/common/ByteArrayExtensions.kt index 2e664ec1..19baef0f 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/common/ByteArrayExtensions.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/common/ByteArrayExtensions.kt @@ -17,8 +17,6 @@ package com.ashampoo.kim.common -import io.ktor.utils.io.charsets.Charsets - private const val FF = 0xFF const val HEX_RADIX = 16 @@ -40,13 +38,6 @@ fun ByteArray.toHex(): String = fun ByteArray.toSingleNumberHexes(): String = joinToString(", ") { "0x" + it.toHex() } -@Suppress("MagicNumber") -fun ByteArray.decodeToIso8859String(): String = - io.ktor.utils.io.core.String( - bytes = this, - charset = Charsets.ISO_8859_1 - ) - fun ByteArray.indexOfNullTerminator(): Int = indexOfNullTerminator(0) diff --git a/src/commonMain/kotlin/com/ashampoo/kim/common/Latin1EncodingExtensions.kt b/src/commonMain/kotlin/com/ashampoo/kim/common/Latin1EncodingExtensions.kt new file mode 100644 index 00000000..45cf3a48 --- /dev/null +++ b/src/commonMain/kotlin/com/ashampoo/kim/common/Latin1EncodingExtensions.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2023 Ashampoo GmbH & Co. KG + * + * 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.common + +expect fun ByteArray.decodeLatin1BytesToString(): String + +expect fun String.encodeToLatin1Bytes(): ByteArray diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/jpeg/iptc/IptcParser.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/jpeg/iptc/IptcParser.kt index 197f8980..b66309ba 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/jpeg/iptc/IptcParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/jpeg/iptc/IptcParser.kt @@ -18,7 +18,7 @@ package com.ashampoo.kim.format.jpeg.iptc import com.ashampoo.kim.common.ByteOrder import com.ashampoo.kim.common.ImageReadException -import com.ashampoo.kim.common.decodeToIso8859String +import com.ashampoo.kim.common.decodeLatin1BytesToString import com.ashampoo.kim.common.slice import com.ashampoo.kim.common.startsWith import com.ashampoo.kim.common.toInt @@ -143,7 +143,7 @@ object IptcParser { value = if (isUtf8) recordData.decodeToString() else - recordData.decodeToIso8859String() + recordData.decodeLatin1BytesToString() ) ) } diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkItxt.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkItxt.kt index 6ea0b8bb..1a5258f3 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkItxt.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkItxt.kt @@ -17,7 +17,7 @@ package com.ashampoo.kim.format.png.chunks import com.ashampoo.kim.common.ImageReadException -import com.ashampoo.kim.common.decodeToIso8859String +import com.ashampoo.kim.common.decodeLatin1BytesToString import com.ashampoo.kim.common.decompress import com.ashampoo.kim.common.indexOfNullTerminator import com.ashampoo.kim.common.slice @@ -51,7 +51,7 @@ class PngChunkItxt( keyword = bytes.slice( startIndex = 0, count = terminatorIndex - ).decodeToIso8859String() + ).decodeLatin1BytesToString() var index = terminatorIndex + 1 @@ -75,7 +75,7 @@ class PngChunkItxt( languageTag = bytes.copyOfRange( fromIndex = index, toIndex = terminatorIndex - ).decodeToIso8859String() + ).decodeLatin1BytesToString() index = terminatorIndex + 1 diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkText.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkText.kt index d4b4de9a..ba92a1c5 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkText.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkText.kt @@ -17,7 +17,7 @@ package com.ashampoo.kim.format.png.chunks import com.ashampoo.kim.common.ImageReadException -import com.ashampoo.kim.common.decodeToIso8859String +import com.ashampoo.kim.common.decodeLatin1BytesToString import com.ashampoo.kim.common.indexOfNullTerminator import com.ashampoo.kim.format.png.ChunkType @@ -44,14 +44,14 @@ class PngChunkText( keyword = bytes.copyOfRange( fromIndex = 0, toIndex = index - ).decodeToIso8859String() + ).decodeLatin1BytesToString() val textLength = bytes.size - (index + 1) text = bytes.copyOfRange( fromIndex = index + 1, toIndex = textLength - ).decodeToIso8859String() + ).decodeLatin1BytesToString() } override fun getKeyword(): String = diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkZtxt.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkZtxt.kt index abac2167..c96d1352 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkZtxt.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/png/chunks/PngChunkZtxt.kt @@ -17,7 +17,7 @@ package com.ashampoo.kim.format.png.chunks import com.ashampoo.kim.common.ImageReadException -import com.ashampoo.kim.common.decodeToIso8859String +import com.ashampoo.kim.common.decodeLatin1BytesToString import com.ashampoo.kim.common.decompress import com.ashampoo.kim.common.indexOfNullTerminator import com.ashampoo.kim.format.png.ChunkType @@ -46,7 +46,7 @@ class PngChunkZtxt( keyword = bytes.copyOfRange( fromIndex = 0, toIndex = index - ).decodeToIso8859String() + ).decodeLatin1BytesToString() index++ diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/taginfos/TagInfoGpsText.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/taginfos/TagInfoGpsText.kt index f9764a6e..03ba5091 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/taginfos/TagInfoGpsText.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/taginfos/TagInfoGpsText.kt @@ -19,15 +19,13 @@ package com.ashampoo.kim.format.tiff.taginfos import com.ashampoo.kim.common.ByteOrder import com.ashampoo.kim.common.ImageReadException import com.ashampoo.kim.common.ImageWriteException -import com.ashampoo.kim.common.decodeToIso8859String +import com.ashampoo.kim.common.decodeLatin1BytesToString +import com.ashampoo.kim.common.encodeToLatin1Bytes import com.ashampoo.kim.common.isEquals +import com.ashampoo.kim.common.slice import com.ashampoo.kim.format.tiff.TiffField import com.ashampoo.kim.format.tiff.constants.TiffDirectoryType import com.ashampoo.kim.format.tiff.fieldtypes.FieldType -import io.ktor.utils.io.charsets.Charset -import io.ktor.utils.io.charsets.Charsets -import io.ktor.utils.io.core.String -import io.ktor.utils.io.core.toByteArray /** * Used by some GPS tags and the EXIF user comment tag, @@ -51,19 +49,19 @@ class TagInfoGpsText( if (value !is String) throw ImageWriteException("GPS text value not String: $value") - val asciiBytes = value.toByteArray(Charsets.ISO_8859_1) + val asciiBytes = value.encodeToLatin1Bytes() - val result = ByteArray(asciiBytes.size + TEXT_ENCODING_ASCII.prefix.size) + val result = ByteArray(asciiBytes.size + TEXT_ENCODING_ASCII_BYTES.size) - TEXT_ENCODING_ASCII.prefix.copyInto( + TEXT_ENCODING_ASCII_BYTES.copyInto( destination = result, destinationOffset = 0, - endIndex = TEXT_ENCODING_ASCII.prefix.size + endIndex = TEXT_ENCODING_ASCII_BYTES.size ) asciiBytes.copyInto( destination = result, - destinationOffset = TEXT_ENCODING_ASCII.prefix.size, + destinationOffset = TEXT_ENCODING_ASCII_BYTES.size, endIndex = asciiBytes.size ) @@ -77,71 +75,75 @@ class TagInfoGpsText( if (fieldType === FieldType.ASCII) return FieldType.ASCII.getValue(entry) - if (fieldType === FieldType.UNDEFINED) { - /* TODO Handle */ - } else if (fieldType === FieldType.BYTE) { - /* TODO Handle */ - } else { + if (fieldType !== FieldType.UNDEFINED && fieldType !== FieldType.BYTE) throw ImageReadException("GPS text field not encoded as bytes.") - } val bytes = entry.byteArrayValue + if (bytes.all { it == ZERO_BYTE }) + return "" + /* Try ASCII with NO prefix. */ - if (bytes.size < 8) - return bytes.decodeToIso8859String() + if (bytes.size < TEXT_ENCODING_BYTE_LENGTH) + return bytes.decodeLatin1BytesToString() + + val encodingPrefixBytes = bytes.slice( + startIndex = 0, + count = TEXT_ENCODING_BYTE_LENGTH + ) - for (encoding in TEXT_ENCODINGS) { + val hasEncoding = + encodingPrefixBytes.contentEquals(TEXT_ENCODING_ASCII_BYTES) || + encodingPrefixBytes.contentEquals(TEXT_ENCODING_UNDEFINED_BYTES) - if (bytes.isEquals(0, encoding.prefix, 0, encoding.prefix.size)) { + if (hasEncoding) { - if (!Charset.isSupported(encoding.encodingName)) - throw ImageWriteException("No support for charset ${encoding.encodingName}") + val bytesWithoutPrefix = bytes.copyOfRange( + fromIndex = TEXT_ENCODING_BYTE_LENGTH, + toIndex = bytes.size + ) - val charset = Charset.forName(encoding.encodingName) + if (bytesWithoutPrefix.all { it == ZERO_BYTE }) + return "" - val decodedString = String( - bytes, - encoding.prefix.size, - bytes.size - encoding.prefix.size, - charset - ) + val decodedString = bytesWithoutPrefix.decodeLatin1BytesToString() - val reEncodedBytes = decodedString.toByteArray(charset) + val reEncodedBytes = decodedString.encodeToLatin1Bytes() - val bytesEqual = bytes.isEquals( - encoding.prefix.size, - reEncodedBytes, - 0, - reEncodedBytes.size - ) + val bytesEqual = bytes.isEquals( + start = TEXT_ENCODING_BYTE_LENGTH, + other = reEncodedBytes, + otherStart = 0, + length = reEncodedBytes.size + ) - if (bytesEqual) - return decodedString - } + if (bytesEqual) + return decodedString } - return bytes.decodeToIso8859String() + return bytes.decodeLatin1BytesToString() } companion object { - /* - * This byte sequence is for US-ASCII, but that's not supported - * in Ktor IO. Therefore we use ISO-8859-1 as a replacement. - */ - private val TEXT_ENCODING_ASCII = TextEncoding( - byteArrayOf(0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00), - "ISO-8859-1" - ) + private const val ZERO_BYTE: Byte = 0.toByte() - // Undefined - // Try to interpret an undefined text as ISO-8859-1 (Latin) - private val TEXT_ENCODING_UNDEFINED = TextEncoding( - byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), - "ISO-8859-1" - ) + private const val TEXT_ENCODING_BYTE_LENGTH = 8 + + /** + * Code for US-ASCII. + * + * This is a subset of ISO-8859-1 (Latin), so we can use that. + */ + private val TEXT_ENCODING_ASCII_BYTES = + byteArrayOf(0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00) - private val TEXT_ENCODINGS = listOf(TEXT_ENCODING_ASCII, TEXT_ENCODING_UNDEFINED) + /* + * Undefined + * + * Try to interpret an undefined text as ISO-8859-1 (Latin) + */ + private val TEXT_ENCODING_UNDEFINED_BYTES = + byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) } } diff --git a/src/commonMain/kotlin/com/ashampoo/kim/input/ByteReader.kt b/src/commonMain/kotlin/com/ashampoo/kim/input/ByteReader.kt index 09230eb6..805923a9 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/input/ByteReader.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/input/ByteReader.kt @@ -20,7 +20,6 @@ import com.ashampoo.kim.common.ImageReadException import com.ashampoo.kim.common.quadsToByteArray import com.ashampoo.kim.common.toHex import com.ashampoo.kim.output.ByteArrayByteWriter -import io.ktor.utils.io.core.Closeable @Suppress("TooManyFunctions", "ComplexInterface", "MagicNumber") interface ByteReader : Closeable { diff --git a/src/commonMain/kotlin/com/ashampoo/kim/input/Closeable.kt b/src/commonMain/kotlin/com/ashampoo/kim/input/Closeable.kt new file mode 100644 index 00000000..a445ae77 --- /dev/null +++ b/src/commonMain/kotlin/com/ashampoo/kim/input/Closeable.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Ashampoo GmbH & Co. KG + * + * 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.input + +fun interface Closeable { + fun close() +} + +/** Executes the code and closes the source after. */ +public inline fun CLOSEABLE.use( + block: (CLOSEABLE) -> RESULT +): RESULT = + try { + block(this) + } finally { + close() + } diff --git a/src/commonMain/kotlin/com/ashampoo/kim/output/ByteArrayByteWriter.kt b/src/commonMain/kotlin/com/ashampoo/kim/output/ByteArrayByteWriter.kt index 67aac66e..a3e7a9e7 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/output/ByteArrayByteWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/output/ByteArrayByteWriter.kt @@ -16,8 +16,8 @@ */ package com.ashampoo.kim.output +import com.ashampoo.kim.input.Closeable import com.ashampoo.kim.input.DEFAULT_BUFFER_SIZE -import io.ktor.utils.io.core.Closeable class ByteArrayByteWriter : ByteWriter, Closeable { diff --git a/src/commonMain/kotlin/com/ashampoo/kim/output/ByteWriter.kt b/src/commonMain/kotlin/com/ashampoo/kim/output/ByteWriter.kt index 90e4a996..398da7c6 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/output/ByteWriter.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/output/ByteWriter.kt @@ -15,7 +15,7 @@ */ package com.ashampoo.kim.output -import io.ktor.utils.io.core.Closeable +import com.ashampoo.kim.input.Closeable interface ByteWriter : Closeable { diff --git a/src/commonTest/kotlin/com/ashampoo/kim/common/ByteArrayExtensionsTest.kt b/src/commonTest/kotlin/com/ashampoo/kim/common/ByteArrayExtensionsTest.kt index 631309a5..1879883a 100644 --- a/src/commonTest/kotlin/com/ashampoo/kim/common/ByteArrayExtensionsTest.kt +++ b/src/commonTest/kotlin/com/ashampoo/kim/common/ByteArrayExtensionsTest.kt @@ -113,53 +113,4 @@ class ByteArrayExtensionsTest { ).toSingleNumberHexes() ) } - - @Test - fun testDecodeToIso8859String() { - - assertEquals( - "RIFF", - byteArrayOf( - 0x52, 0x49, 0x46, 0x46 - ).decodeToIso8859String() - ) - - assertEquals( - "WEBP", - byteArrayOf( - 0x57, 0x45, 0x42, 0x50 - ).decodeToIso8859String() - ) - - assertEquals( - "FUJIFILMCCD-RAW", - byteArrayOf( - 0x46, 0x55, 0x4A, 0x49, 0x46, 0x49, 0x4C, 0x4D, - 0x43, 0x43, 0x44, 0x2D, 0x52, 0x41, 0x57 - ).decodeToIso8859String() - ) - - /* ISO 8859-1 bytes */ - assertEquals( - "Äußerst öffentliches Ü!", - byteArrayOf( - 0xC4.toByte(), 0x75, 0xDF.toByte(), 0x65, 0x72, - 0x73, 0x74, 0x20, 0xF6.toByte(), 0x66, 0x66, - 0x65, 0x6E, 0x74, 0x6C, 0x69, 0x63, 0x68, 0x65, - 0x73, 0x20, 0xDC.toByte(), 0x21 - ).decodeToIso8859String() - ) - - /* Just for comparison the UTF-8 bytes. */ - assertEquals( - "Äußerst öffentliches Ü!", - byteArrayOf( - 0xC3.toByte(), 0x84.toByte(), 0x75, 0xC3.toByte(), - 0x9F.toByte(), 0x65, 0x72, 0x73, 0x74, 0x20, - 0xC3.toByte(), 0xB6.toByte(), 0x66, 0x66, 0x65, - 0x6E, 0x74, 0x6C, 0x69, 0x63, 0x68, 0x65, 0x73, - 0x20, 0xC3.toByte(), 0x9C.toByte(), 0x21 - ).decodeToString() - ) - } } diff --git a/src/commonTest/kotlin/com/ashampoo/kim/common/Latin1EncodingTest.kt b/src/commonTest/kotlin/com/ashampoo/kim/common/Latin1EncodingTest.kt new file mode 100644 index 00000000..4173149f --- /dev/null +++ b/src/commonTest/kotlin/com/ashampoo/kim/common/Latin1EncodingTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright 2023 Ashampoo GmbH & Co. KG + * + * 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.common + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class Latin1EncodingTest { + + private val shortTestString = "Äußerst öffentliches Ü!" + + private val longTestString = " !\"#$%&'()*+,-./0123456789:;<=>?@" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + + "¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ" + + "×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + + private val nonLatin1TestString = "abc123€αβγδ★" + + private val shortTestStringLatin1Bytes: ByteArray = + byteArrayOf( + 0xC4.toByte(), 0x75, 0xDF.toByte(), 0x65, 0x72, + 0x73, 0x74, 0x20, 0xF6.toByte(), 0x66, 0x66, + 0x65, 0x6E, 0x74, 0x6C, 0x69, 0x63, 0x68, 0x65, + 0x73, 0x20, 0xDC.toByte(), 0x21 + ) + + private val shortTestStringUtf8Bytes: ByteArray = + byteArrayOf( + 0xC3.toByte(), 0x84.toByte(), 0x75, 0xC3.toByte(), + 0x9F.toByte(), 0x65, 0x72, 0x73, 0x74, 0x20, + 0xC3.toByte(), 0xB6.toByte(), 0x66, 0x66, 0x65, + 0x6E, 0x74, 0x6C, 0x69, 0x63, 0x68, 0x65, 0x73, + 0x20, 0xC3.toByte(), 0x9C.toByte(), 0x21 + ) + + private val longTestStringLatin1Bytes: ByteArray = + intArrayOf( + 0X20, 0X21, 0X22, 0X23, 0X24, 0X25, 0X26, 0X27, 0X28, 0X29, 0X2A, 0X2B, 0X2C, 0X2D, + 0X2E, 0X2F, 0X30, 0X31, 0X32, 0X33, 0X34, 0X35, 0X36, 0X37, 0X38, 0X39, 0X3A, 0X3B, + 0X3C, 0X3D, 0X3E, 0X3F, 0X40, 0X41, 0X42, 0X43, 0X44, 0X45, 0X46, 0X47, 0X48, 0X49, + 0X4A, 0X4B, 0X4C, 0X4D, 0X4E, 0X4F, 0X50, 0X51, 0X52, 0X53, 0X54, 0X55, 0X56, 0X57, + 0X58, 0X59, 0X5A, 0X5B, 0X5C, 0X5D, 0X5E, 0X5F, 0X60, 0X61, 0X62, 0X63, 0X64, 0X65, + 0X66, 0X67, 0X68, 0X69, 0X6A, 0X6B, 0X6C, 0X6D, 0X6E, 0X6F, 0X70, 0X71, 0X72, 0X73, + 0X74, 0X75, 0X76, 0X77, 0X78, 0X79, 0X7A, 0X7B, 0X7C, 0X7D, 0X7E, 0XA1, 0XA2, 0XA3, + 0XA4, 0XA5, 0XA6, 0XA7, 0XA8, 0XA9, 0XAA, 0XAB, 0XAC, 0XAE, 0XAF, 0XB0, 0XB1, 0XB2, + 0XB3, 0XB4, 0XB5, 0XB6, 0XB7, 0XB8, 0XB9, 0XBA, 0XBB, 0XBC, 0XBD, 0XBE, 0XBF, 0XC0, + 0XC1, 0XC2, 0XC3, 0XC4, 0XC5, 0XC6, 0XC7, 0XC8, 0XC9, 0XCA, 0XCB, 0XCC, 0XCD, 0XCE, + 0XCF, 0XD0, 0XD1, 0XD2, 0XD3, 0XD4, 0XD5, 0XD6, 0XD7, 0XD8, 0XD9, 0XDA, 0XDB, 0XDC, + 0XDD, 0XDE, 0XDF, 0XE0, 0XE1, 0XE2, 0XE3, 0XE4, 0XE5, 0XE6, 0XE7, 0XE8, 0XE9, 0XEA, + 0XEB, 0XEC, 0XED, 0XEE, 0XEF, 0XF0, 0XF1, 0XF2, 0XF3, 0XF4, 0XF5, 0XF6, 0XF7, 0XF8, + 0XF9, 0XFA, 0XFB, 0XFC, 0XFD, 0XFE, 0XFF + ).map { it.toByte() }.toByteArray() + + private val longTestStringUtf8Bytes: ByteArray = + intArrayOf( + 0X20, 0X21, 0X22, 0X23, 0X24, 0X25, 0X26, 0X27, 0X28, 0X29, 0X2A, 0X2B, 0X2C, 0X2D, + 0X2E, 0X2F, 0X30, 0X31, 0X32, 0X33, 0X34, 0X35, 0X36, 0X37, 0X38, 0X39, 0X3A, 0X3B, + 0X3C, 0X3D, 0X3E, 0X3F, 0X40, 0X41, 0X42, 0X43, 0X44, 0X45, 0X46, 0X47, 0X48, 0X49, + 0X4A, 0X4B, 0X4C, 0X4D, 0X4E, 0X4F, 0X50, 0X51, 0X52, 0X53, 0X54, 0X55, 0X56, 0X57, + 0X58, 0X59, 0X5A, 0X5B, 0X5C, 0X5D, 0X5E, 0X5F, 0X60, 0X61, 0X62, 0X63, 0X64, 0X65, + 0X66, 0X67, 0X68, 0X69, 0X6A, 0X6B, 0X6C, 0X6D, 0X6E, 0X6F, 0X70, 0X71, 0X72, 0X73, + 0X74, 0X75, 0X76, 0X77, 0X78, 0X79, 0X7A, 0X7B, 0X7C, 0X7D, 0X7E, 0XC2, 0XA1, 0XC2, + 0XA2, 0XC2, 0XA3, 0XC2, 0XA4, 0XC2, 0XA5, 0XC2, 0XA6, 0XC2, 0XA7, 0XC2, 0XA8, 0XC2, + 0XA9, 0XC2, 0XAA, 0XC2, 0XAB, 0XC2, 0XAC, 0XC2, 0XAE, 0XC2, 0XAF, 0XC2, 0XB0, 0XC2, + 0XB1, 0XC2, 0XB2, 0XC2, 0XB3, 0XC2, 0XB4, 0XC2, 0XB5, 0XC2, 0XB6, 0XC2, 0XB7, 0XC2, + 0XB8, 0XC2, 0XB9, 0XC2, 0XBA, 0XC2, 0XBB, 0XC2, 0XBC, 0XC2, 0XBD, 0XC2, 0XBE, 0XC2, + 0XBF, 0XC3, 0X80, 0XC3, 0X81, 0XC3, 0X82, 0XC3, 0X83, 0XC3, 0X84, 0XC3, 0X85, 0XC3, + 0X86, 0XC3, 0X87, 0XC3, 0X88, 0XC3, 0X89, 0XC3, 0X8A, 0XC3, 0X8B, 0XC3, 0X8C, 0XC3, + 0X8D, 0XC3, 0X8E, 0XC3, 0X8F, 0XC3, 0X90, 0XC3, 0X91, 0XC3, 0X92, 0XC3, 0X93, 0XC3, + 0X94, 0XC3, 0X95, 0XC3, 0X96, 0XC3, 0X97, 0XC3, 0X98, 0XC3, 0X99, 0XC3, 0X9A, 0XC3, + 0X9B, 0XC3, 0X9C, 0XC3, 0X9D, 0XC3, 0X9E, 0XC3, 0X9F, 0XC3, 0XA0, 0XC3, 0XA1, 0XC3, + 0XA2, 0XC3, 0XA3, 0XC3, 0XA4, 0XC3, 0XA5, 0XC3, 0XA6, 0XC3, 0XA7, 0XC3, 0XA8, 0XC3, + 0XA9, 0XC3, 0XAA, 0XC3, 0XAB, 0XC3, 0XAC, 0XC3, 0XAD, 0XC3, 0XAE, 0XC3, 0XAF, 0XC3, + 0XB0, 0XC3, 0XB1, 0XC3, 0XB2, 0XC3, 0XB3, 0XC3, 0XB4, 0XC3, 0XB5, 0XC3, 0XB6, 0XC3, + 0XB7, 0XC3, 0XB8, 0XC3, 0XB9, 0XC3, 0XBA, 0XC3, 0XBB, 0XC3, 0XBC, 0XC3, 0XBD, 0XC3, + 0XBE, 0XC3, 0XBF + ).map { it.toByte() }.toByteArray() + + private val nonLatin1TestStringLatin1Bytes: ByteArray = + intArrayOf( + 0X61, 0X62, 0X63, 0X31, 0X32, 0X33, + 0X3F, 0X3F, 0X3F, 0X3F, 0X3F, 0X3F + ).map { it.toByte() }.toByteArray() + + private val nonLatin1TestStringUtf8Bytes: ByteArray = + intArrayOf( + 0X61, 0X62, 0X63, 0X31, 0X32, 0X33, 0XE2, 0X82, 0XAC, 0XCE, + 0XB1, 0XCE, 0XB2, 0XCE, 0XB3, 0XCE, 0XB4, 0XE2, 0X98, 0X85 + ).map { it.toByte() }.toByteArray() + + @Test + fun testEncodeToLatin1Bytes() { + + assertContentEquals( + expected = shortTestStringLatin1Bytes, + actual = shortTestString.encodeToLatin1Bytes() + ) + + assertContentEquals( + expected = longTestStringLatin1Bytes, + actual = longTestString.encodeToLatin1Bytes() + ) + + // Fails on Apple systems with MalformedInputException +// assertContentEquals( +// expected = nonLatin1TestStringLatin1Bytes, +// actual = nonLatin1TestString.encodeToLatin1Bytes() +// ) + } + + @Test + fun testEncodeToUtf8Bytes() { + + assertContentEquals( + expected = shortTestStringUtf8Bytes, + actual = shortTestString.encodeToByteArray() + ) + + assertContentEquals( + expected = longTestStringUtf8Bytes, + actual = longTestString.encodeToByteArray() + ) + + assertContentEquals( + expected = nonLatin1TestStringUtf8Bytes, + actual = nonLatin1TestString.encodeToByteArray() + ) + } + + @Test + fun testDecodeLatin1BytesToString() { + + assertEquals( + expected = shortTestString, + actual = shortTestStringLatin1Bytes.decodeLatin1BytesToString() + ) + + assertEquals( + expected = longTestString, + actual = longTestStringLatin1Bytes.decodeLatin1BytesToString() + ) + + assertEquals( + expected = "abc123??????", + actual = nonLatin1TestStringLatin1Bytes.decodeLatin1BytesToString() + ) + } + + /* Just for comparison the UTF-8 bytes. */ + @Test + fun testDecodeUtf8BytesToString() { + + assertEquals( + expected = shortTestString, + actual = shortTestStringUtf8Bytes.decodeToString() + ) + + assertEquals( + expected = longTestString, + actual = longTestStringUtf8Bytes.decodeToString() + ) + + assertEquals( + expected = nonLatin1TestString, + actual = nonLatin1TestStringUtf8Bytes.decodeToString() + ) + } +} diff --git a/src/commonTest/resources/com/ashampoo/kim/testdata/full/photo_21.html b/src/commonTest/resources/com/ashampoo/kim/testdata/full/photo_21.html index 749bf771..37c7a068 100644 --- a/src/commonTest/resources/com/ashampoo/kim/testdata/full/photo_21.html +++ b/src/commonTest/resources/com/ashampoo/kim/testdata/full/photo_21.html @@ -8,15 +8,51 @@