diff --git a/plugin/src/main/kotlin/xyz/block/microfilm/decompression/DecompressTask.kt b/plugin/src/main/kotlin/xyz/block/microfilm/decompression/DecompressTask.kt index 138b6d4..827d6a6 100644 --- a/plugin/src/main/kotlin/xyz/block/microfilm/decompression/DecompressTask.kt +++ b/plugin/src/main/kotlin/xyz/block/microfilm/decompression/DecompressTask.kt @@ -19,13 +19,25 @@ import okio.FileSystem import okio.Path.Companion.toOkioPath import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option import org.gradle.work.DisableCachingByDefault +import xyz.block.microfilm.Manifest import xyz.block.microfilm.scanning.RealScanner +import xyz.block.microfilm.writeManifest @DisableCachingByDefault(because = "This task modifies the source tree in place") internal abstract class DecompressTask : DefaultTask() { + @get:Internal + @get:Option( + option = "images", + description = + "Decompress the images matching these glob patterns, relative to the res directory.", + ) + abstract val imagePatterns: ListProperty + @get:Internal abstract val microfilmDirectory: DirectoryProperty @get:Internal abstract val resourcesDirectory: DirectoryProperty @@ -54,11 +66,18 @@ internal abstract class DecompressTask : DefaultTask() { ) // Decompress the images - scanner.scan().forEach { imageGroup -> - decompressor.decompress(imageGroup = imageGroup) - } + val manifestEntries = + decompressor.decompress( + scanner = scanner, + imagePatterns = imagePatterns.get().ifEmpty { listOf("**") }, + resourcesDirectory = resourcesDirectoryPath, + microfilmDirectory = microfilmDirectoryPath, + ) + + // Create the manifest from remaining images + val manifest = Manifest(entries = manifestEntries.sortedBy { entry -> entry.sourcePath }) - // Delete the manifest - fileSystem.delete(path = microfilmManifestPath) + // Write the manifest to disk + fileSystem.writeManifest(path = microfilmManifestPath, manifest = manifest) } } diff --git a/plugin/src/main/kotlin/xyz/block/microfilm/decompression/Decompressor.kt b/plugin/src/main/kotlin/xyz/block/microfilm/decompression/Decompressor.kt index e72e340..630d4c6 100644 --- a/plugin/src/main/kotlin/xyz/block/microfilm/decompression/Decompressor.kt +++ b/plugin/src/main/kotlin/xyz/block/microfilm/decompression/Decompressor.kt @@ -15,7 +15,12 @@ */ package xyz.block.microfilm.decompression +import okio.Path +import okio.Path.Companion.toPath +import xyz.block.microfilm.Manifest +import xyz.block.microfilm.matchesGlobs import xyz.block.microfilm.scanning.ImageGroup +import xyz.block.microfilm.scanning.Scanner /** * A decompressor that removes compressed images and restores their original uncompressed @@ -25,3 +30,25 @@ internal interface Decompressor { /** Removes the given compressed image and restores its original uncompressed counterpart. */ fun decompress(imageGroup: ImageGroup) } + +/** Scans for images and decompresses them. */ +internal fun Decompressor.decompress( + scanner: Scanner, + imagePatterns: List, + resourcesDirectory: Path, + microfilmDirectory: Path, +): List { + return scanner.scan().mapNotNull { imageGroup -> + val pngPath = + imageGroup.microfilmManifestEntry?.sourcePath?.toPath() + ?: imageGroup.microfilmPng?.relativeTo(other = microfilmDirectory) + ?: imageGroup.resourcesPng?.relativeTo(other = resourcesDirectory) + val webpPath = imageGroup.resourcesWebp?.relativeTo(other = resourcesDirectory) + if (listOfNotNull(pngPath, webpPath).matchesGlobs(patterns = imagePatterns)) { + decompress(imageGroup = imageGroup) + null + } else { + imageGroup.microfilmManifestEntry + } + } +} diff --git a/plugin/src/test/kotlin/xyz/block/microfilm/decompression/DecompressorTest.kt b/plugin/src/test/kotlin/xyz/block/microfilm/decompression/DecompressorTest.kt new file mode 100644 index 0000000..11b1291 --- /dev/null +++ b/plugin/src/test/kotlin/xyz/block/microfilm/decompression/DecompressorTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2026 Block, Inc. + * + * 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 xyz.block.microfilm.decompression + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import org.junit.jupiter.api.Test +import xyz.block.microfilm.scanning.FakeScanner +import xyz.block.microfilm.scanning.ImageGroup +import xyz.block.microfilm.scanning.ImageGroupFixtures.LOSSY_IMAGE_GROUP +import xyz.block.microfilm.scanning.ImageGroupFixtures.MICROFILM_DIRECTORY +import xyz.block.microfilm.scanning.ImageGroupFixtures.RELATIVE_PNG +import xyz.block.microfilm.scanning.ImageGroupFixtures.RELATIVE_WEBP +import xyz.block.microfilm.scanning.ImageGroupFixtures.RESOURCES_DIRECTORY + +class DecompressorTest { + @Test + fun `decompress respects image patterns`() { + assertDecompress( + imageGroup = LOSSY_IMAGE_GROUP, + imagePattern = RELATIVE_PNG, + expected = true, + ) + + assertDecompress( + imageGroup = LOSSY_IMAGE_GROUP, + imagePattern = "other.png", + expected = false, + ) + + assertDecompress( + imageGroup = LOSSY_IMAGE_GROUP, + imagePattern = RELATIVE_WEBP, + expected = true, + ) + + assertDecompress( + imageGroup = LOSSY_IMAGE_GROUP, + imagePattern = "other.webp", + expected = false, + ) + } + + private fun assertDecompress( + imageGroup: ImageGroup, + imagePattern: String = "**", + expected: Boolean, + ) { + val decompressor = FakeDecompressor() + val scanner = FakeScanner().apply { scanResponses.add(listOf(imageGroup)) } + + val manifestEntries = + decompressor.decompress( + scanner = scanner, + imagePatterns = listOf(imagePattern), + resourcesDirectory = RESOURCES_DIRECTORY, + microfilmDirectory = MICROFILM_DIRECTORY, + ) + + if (expected) { + assertThat(decompressor.decompressRequests).containsExactly(imageGroup) + assertThat(manifestEntries).isEmpty() + } else { + assertThat(decompressor.decompressRequests).isEmpty() + assertThat(manifestEntries).containsExactly(imageGroup.microfilmManifestEntry) + } + } +} diff --git a/plugin/src/test/kotlin/xyz/block/microfilm/decompression/FakeDecompressor.kt b/plugin/src/test/kotlin/xyz/block/microfilm/decompression/FakeDecompressor.kt new file mode 100644 index 0000000..653544e --- /dev/null +++ b/plugin/src/test/kotlin/xyz/block/microfilm/decompression/FakeDecompressor.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2026 Block, Inc. + * + * 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 xyz.block.microfilm.decompression + +import xyz.block.microfilm.scanning.ImageGroup + +internal class FakeDecompressor : Decompressor { + val decompressRequests = ArrayDeque() + + override fun decompress(imageGroup: ImageGroup) { + decompressRequests.add(imageGroup) + } +}