[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\nko_fi: zetbaitsu\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/build\n/captures\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: android\ndist: trusty\nandroid:\n  components:\n    # Uncomment the lines below if you want to\n    # use the latest revision of Android SDK Tools\n    # - tools\n    # - platform-tools\n\n    # The BuildTools version used by your project\n    - build-tools-29.0.2\n\n    # The SDK version used to compile your project\n    - android-29\n\n    # Additional components\n    - extra-google-google_play_services\n    - extra-google-m2repository\n    - extra-android-m2repository\n\n    # Specify at least one system image,\n    # if you need to run emulator(s) during your tests\n    - sys-img-x86-android-21\nbefore_script:\n  - touch local.properties\nscript:\n  - ./gradlew clean build jacocoTestReleaseUnitTestReport\nafter_success:\n  - bash <(curl -s https://codecov.io/bash)"
  },
  {
    "path": "README.md",
    "content": "Compressor\n======\n[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Compressor-blue.svg?style=flat)](http://android-arsenal.com/details/1/3758)\n[![Build Status](https://travis-ci.org/zetbaitsu/Compressor.svg?branch=master)](https://travis-ci.org/zetbaitsu/Compressor)\n[![codecov](https://codecov.io/gh/zetbaitsu/Compressor/branch/master/graph/badge.svg)](https://codecov.io/gh/zetbaitsu/Compressor)\n<p align=\"center\"><img src=\"https://raw.githubusercontent.com/zetbaitsu/Compressor/master/ss.png\" width=\"50%\" /></p>\nCompressor is a lightweight and powerful android image compression library. Compressor will allow you to compress large photos into smaller sized photos with very less or negligible loss in quality of the image.\n\n# Gradle\n```groovy\ndependencies {\n    implementation 'id.zelory:compressor:3.0.1'\n}\n```\n# Let's compress the image size!\n#### Compress Image File\n```kotlin\nval compressedImageFile = Compressor.compress(context, actualImageFile)\n```\n#### Compress Image File to specific destination\n```kotlin\nval compressedImageFile = Compressor.compress(context, actualImageFile) {\n    default()\n    destination(myFile)\n}\n```\n### I want custom Compressor!\n#### Using default constraint and custom partial of it\n```kotlin\nval compressedImageFile = Compressor.compress(context, actualImageFile) {\n    default(width = 640, format = Bitmap.CompressFormat.WEBP)\n}\n```\n#### Full custom constraint\n```kotlin\nval compressedImageFile = Compressor.compress(context, actualImageFile) {\n    resolution(1280, 720)\n    quality(80)\n    format(Bitmap.CompressFormat.WEBP)\n    size(2_097_152) // 2 MB\n}\n```\n#### Using your own custom constraint\n```kotlin\nclass MyLowerCaseNameConstraint: Constraint {\n    override fun isSatisfied(imageFile: File): Boolean {\n        return imageFile.name.all { it.isLowerCase() }\n    }\n\n    override fun satisfy(imageFile: File): File {\n        val destination = File(imageFile.parent, imageFile.name.toLowerCase())\n        imageFile.renameTo(destination)\n        return destination\n    }\n}\n\nval compressedImageFile = Compressor.compress(context, actualImageFile) {\n    constraint(MyLowerCaseNameConstraint()) // your own constraint\n    quality(80) // combine with compressor constraint\n    format(Bitmap.CompressFormat.WEBP)\n}\n```\n#### You can create your own extension too\n```kotlin\nfun Compression.lowerCaseName() {\n    constraint(MyLowerCaseNameConstraint())\n}\n\nval compressedImageFile = Compressor.compress(context, actualImageFile) {\n    lowerCaseName() // your own extension\n    quality(80) // combine with compressor constraint\n    format(Bitmap.CompressFormat.WEBP)\n}\n```\n\n### Compressor now is using Kotlin coroutines!\n#### Calling Compressor should be done from coroutines scope\n```kotlin\n// e.g calling from activity lifecycle scope\nlifecycleScope.launch {\n    val compressedImageFile = Compressor.compress(context, actualImageFile)\n}\n\n// calling from global scope\nGlobalScope.launch {\n    val compressedImageFile = Compressor.compress(context, actualImageFile)\n}\n```\n#### Run Compressor in main thread\n```kotlin\nval compressedImageFile = Compressor.compress(context, actualImageFile, Dispatchers.Main)\n```\n\n### Old version\nPlease read this [readme](https://github.com/zetbaitsu/Compressor/blob/master/README_v2.md)\n\nLicense\n-------\n    Copyright (c) 2016 Zetra.\n    \n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n"
  },
  {
    "path": "README_v2.md",
    "content": "Compressor\n======\n[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Compressor-blue.svg?style=flat)](http://android-arsenal.com/details/1/3758)\n<p align=\"center\"><img src=\"https://raw.githubusercontent.com/zetbaitsu/Compressor/master/ss.png\" width=\"50%\" /></p>\nCompressor is a lightweight and powerful android image compression library. Compressor will allow you to compress large photos into smaller sized photos with very less or negligible loss in quality of the image.\n\n# Gradle\n```groovy\ndependencies {\n    implementation 'id.zelory:compressor:2.1.1'\n}\n```\n# Let's compress the image size!\n#### Compress Image File\n```java\ncompressedImageFile = new Compressor(this).compressToFile(actualImageFile);\n```\n#### Compress Image File to Bitmap\n```java\ncompressedImageBitmap = new Compressor(this).compressToBitmap(actualImageFile);\n```\n### I want custom Compressor!\n```java\ncompressedImage = new Compressor(this)\n            .setMaxWidth(640)\n            .setMaxHeight(480)\n            .setQuality(75)\n            .setCompressFormat(Bitmap.CompressFormat.WEBP)\n            .setDestinationDirectoryPath(Environment.getExternalStoragePublicDirectory(\n              Environment.DIRECTORY_PICTURES).getAbsolutePath())\n            .compressToFile(actualImage);\n```\n### Stay cool compress image asynchronously with RxJava!\n```java\nnew Compressor(this)\n        .compressToFileAsFlowable(actualImage)\n        .subscribeOn(Schedulers.io())\n        .observeOn(AndroidSchedulers.mainThread())\n        .subscribe(new Consumer<File>() {\n            @Override\n            public void accept(File file) {\n                compressedImage = file;\n            }\n        }, new Consumer<Throwable>() {\n            @Override\n            public void accept(Throwable throwable) {\n                throwable.printStackTrace();\n                showError(throwable.getMessage());\n            }\n        });\n```\n\nLicense\n-------\n    Copyright (c) 2016 Zetra.\n    \n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n"
  },
  {
    "path": "app/.gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/build\n/captures"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-android-extensions'\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion '29.0.2'\n\n    defaultConfig {\n        applicationId \"id.zelory.compressor.sample\"\n        minSdkVersion 14\n        targetSdkVersion 29\n        versionCode 1\n        versionName \"1.0\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled true\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    implementation\"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version\"\n    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'\n    implementation 'androidx.appcompat:appcompat:1.1.0'\n    //implementation project(':compressor')\n    implementation 'id.zelory:compressor:3.0.1'\n\n    testImplementation 'junit:junit:4.12'\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/macbookair/Library/Android/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"id.zelory.compressor.sample\">\n\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\"\n        tools:ignore=\"GoogleAppIndexingWarning\">\n        <activity android:name=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/id/zelory/compressor/sample/FileUtil.java",
    "content": "package id.zelory.compressor.sample;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.net.Uri;\nimport android.provider.OpenableColumns;\nimport android.util.Log;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n/**\n * Created on : June 18, 2016\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass FileUtil {\n    private static final int EOF = -1;\n    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;\n\n    private FileUtil() {\n\n    }\n\n    public static File from(Context context, Uri uri) throws IOException {\n        InputStream inputStream = context.getContentResolver().openInputStream(uri);\n        String fileName = getFileName(context, uri);\n        String[] splitName = splitFileName(fileName);\n        File tempFile = File.createTempFile(splitName[0], splitName[1]);\n        tempFile = rename(tempFile, fileName);\n        tempFile.deleteOnExit();\n        FileOutputStream out = null;\n        try {\n            out = new FileOutputStream(tempFile);\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n        }\n        if (inputStream != null) {\n            copy(inputStream, out);\n            inputStream.close();\n        }\n\n        if (out != null) {\n            out.close();\n        }\n        return tempFile;\n    }\n\n    private static String[] splitFileName(String fileName) {\n        String name = fileName;\n        String extension = \"\";\n        int i = fileName.lastIndexOf(\".\");\n        if (i != -1) {\n            name = fileName.substring(0, i);\n            extension = fileName.substring(i);\n        }\n\n        return new String[]{name, extension};\n    }\n\n    private static String getFileName(Context context, Uri uri) {\n        String result = null;\n        if (uri.getScheme().equals(\"content\")) {\n            Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);\n            try {\n                if (cursor != null && cursor.moveToFirst()) {\n                    result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));\n                }\n            } catch (Exception e) {\n                e.printStackTrace();\n            } finally {\n                if (cursor != null) {\n                    cursor.close();\n                }\n            }\n        }\n        if (result == null) {\n            result = uri.getPath();\n            int cut = result.lastIndexOf(File.separator);\n            if (cut != -1) {\n                result = result.substring(cut + 1);\n            }\n        }\n        return result;\n    }\n\n    private static File rename(File file, String newName) {\n        File newFile = new File(file.getParent(), newName);\n        if (!newFile.equals(file)) {\n            if (newFile.exists() && newFile.delete()) {\n                Log.d(\"FileUtil\", \"Delete old \" + newName + \" file\");\n            }\n            if (file.renameTo(newFile)) {\n                Log.d(\"FileUtil\", \"Rename file to \" + newName);\n            }\n        }\n        return newFile;\n    }\n\n    private static long copy(InputStream input, OutputStream output) throws IOException {\n        long count = 0;\n        int n;\n        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];\n        while (EOF != (n = input.read(buffer))) {\n            output.write(buffer, 0, n);\n            count += n;\n        }\n        return count;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/id/zelory/compressor/sample/MainActivity.kt",
    "content": "package id.zelory.compressor.sample\n\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.os.Environment\nimport android.util.Log\nimport android.widget.Toast\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.lifecycle.lifecycleScope\nimport id.zelory.compressor.Compressor\nimport id.zelory.compressor.constraint.default\nimport id.zelory.compressor.constraint.destination\nimport id.zelory.compressor.constraint.format\nimport id.zelory.compressor.constraint.quality\nimport id.zelory.compressor.constraint.resolution\nimport id.zelory.compressor.constraint.size\nimport id.zelory.compressor.loadBitmap\nimport kotlinx.android.synthetic.main.activity_main.actualImageView\nimport kotlinx.android.synthetic.main.activity_main.actualSizeTextView\nimport kotlinx.android.synthetic.main.activity_main.chooseImageButton\nimport kotlinx.android.synthetic.main.activity_main.compressImageButton\nimport kotlinx.android.synthetic.main.activity_main.compressedImageView\nimport kotlinx.android.synthetic.main.activity_main.compressedSizeTextView\nimport kotlinx.android.synthetic.main.activity_main.customCompressImageButton\nimport kotlinx.coroutines.launch\nimport java.io.File\nimport java.io.IOException\nimport java.text.DecimalFormat\nimport java.util.*\nimport kotlin.math.log10\nimport kotlin.math.pow\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass MainActivity : AppCompatActivity() {\n    companion object {\n        private const val PICK_IMAGE_REQUEST = 1\n    }\n\n    private var actualImage: File? = null\n    private var compressedImage: File? = null\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n        actualImageView.setBackgroundColor(getRandomColor())\n        clearImage()\n        setupClickListener()\n    }\n\n    private fun setupClickListener() {\n        chooseImageButton.setOnClickListener { chooseImage() }\n        compressImageButton.setOnClickListener { compressImage() }\n        customCompressImageButton.setOnClickListener { customCompressImage() }\n    }\n\n    private fun chooseImage() {\n        val intent = Intent(Intent.ACTION_GET_CONTENT)\n        intent.type = \"image/*\"\n        startActivityForResult(intent, PICK_IMAGE_REQUEST)\n    }\n\n    private fun compressImage() {\n        actualImage?.let { imageFile ->\n            lifecycleScope.launch {\n                // Default compression\n                compressedImage = Compressor.compress(this@MainActivity, imageFile)\n                setCompressedImage()\n            }\n        } ?: showError(\"Please choose an image!\")\n    }\n\n    private fun customCompressImage() {\n        actualImage?.let { imageFile ->\n            lifecycleScope.launch {\n                // Default compression with custom destination file\n                /*compressedImage = Compressor.compress(this@MainActivity, imageFile) {\n                    default()\n                    getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.also {\n                        val file = File(\"${it.absolutePath}${File.separator}my_image.${imageFile.extension}\")\n                        destination(file)\n                    }\n                }*/\n\n                // Full custom\n                compressedImage = Compressor.compress(this@MainActivity, imageFile) {\n                    resolution(1280, 720)\n                    quality(80)\n                    format(Bitmap.CompressFormat.WEBP)\n                    size(2_097_152) // 2 MB\n                }\n                setCompressedImage()\n            }\n        } ?: showError(\"Please choose an image!\")\n    }\n\n    private fun setCompressedImage() {\n        compressedImage?.let {\n            compressedImageView.setImageBitmap(BitmapFactory.decodeFile(it.absolutePath))\n            compressedSizeTextView.text = String.format(\"Size : %s\", getReadableFileSize(it.length()))\n            Toast.makeText(this, \"Compressed image save in \" + it.path, Toast.LENGTH_LONG).show()\n            Log.d(\"Compressor\", \"Compressed image save in \" + it.path)\n        }\n    }\n\n    private fun clearImage() {\n        actualImageView.setBackgroundColor(getRandomColor())\n        compressedImageView.setImageDrawable(null)\n        compressedImageView.setBackgroundColor(getRandomColor())\n        compressedSizeTextView.text = \"Size : -\"\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK) {\n            if (data == null) {\n                showError(\"Failed to open picture!\")\n                return\n            }\n            try {\n                actualImage = FileUtil.from(this, data.data)?.also {\n                    actualImageView.setImageBitmap(loadBitmap(it))\n                    actualSizeTextView.text = String.format(\"Size : %s\", getReadableFileSize(it.length()))\n                    clearImage()\n                }\n            } catch (e: IOException) {\n                showError(\"Failed to read picture data!\")\n                e.printStackTrace()\n            }\n        }\n    }\n\n    private fun showError(errorMessage: String) {\n        Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()\n    }\n\n    private fun getRandomColor() = Random().run {\n        Color.argb(100, nextInt(256), nextInt(256), nextInt(256))\n    }\n\n    private fun getReadableFileSize(size: Long): String {\n        if (size <= 0) {\n            return \"0\"\n        }\n        val units = arrayOf(\"B\", \"KB\", \"MB\", \"GB\", \"TB\")\n        val digitGroups = (log10(size.toDouble()) / log10(1024.0)).toInt()\n        return DecimalFormat(\"#,##0.#\").format(size / 1024.0.pow(digitGroups.toDouble())) + \" \" + units[digitGroups]\n    }\n}"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:paddingBottom=\"@dimen/activity_vertical_margin\"\n        android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n        android:paddingRight=\"@dimen/activity_horizontal_margin\"\n        android:paddingTop=\"@dimen/activity_vertical_margin\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <ImageView\n                android:id=\"@+id/actualImageView\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"240dp\"\n                android:layout_marginEnd=\"4dp\"\n                android:layout_marginRight=\"4dp\"\n                android:layout_weight=\"1\"\n                android:adjustViewBounds=\"true\"/>\n\n            <ImageView\n                android:id=\"@+id/compressedImageView\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"240dp\"\n                android:layout_marginLeft=\"4dp\"\n                android:layout_marginStart=\"4dp\"\n                android:layout_weight=\"1\"\n                android:adjustViewBounds=\"true\"/>\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"4dp\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"4dp\"\n                android:layout_marginRight=\"4dp\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\"\n                android:text=\"Actual Image\"\n                android:textSize=\"12sp\"/>\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"4dp\"\n                android:layout_marginStart=\"4dp\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\"\n                android:text=\"Compressed Image\"\n                android:textSize=\"12sp\"/>\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"2dp\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:id=\"@+id/actualSizeTextView\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"4dp\"\n                android:layout_marginRight=\"4dp\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\"\n                android:text=\"Size : -\"\n                android:textSize=\"12sp\"/>\n\n            <TextView\n                android:id=\"@+id/compressedSizeTextView\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"4dp\"\n                android:layout_marginStart=\"4dp\"\n                android:layout_weight=\"1\"\n                android:gravity=\"center\"\n                android:text=\"Size : -\"\n                android:textSize=\"12sp\"/>\n\n        </LinearLayout>\n\n        <Button\n            android:id=\"@+id/chooseImageButton\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"8dp\"\n            android:text=\"Choose image\"/>\n\n        <Button\n            android:id=\"@+id/compressImageButton\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"8dp\"\n            android:text=\"Compress image\"/>\n\n        <Button\n            android:id=\"@+id/customCompressImageButton\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"8dp\"\n            android:text=\"Custom compress\"/>\n\n    </LinearLayout>\n</ScrollView>\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">Compressor</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-w820dp/dimens.xml",
    "content": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n         (such as screen margins) for screens with more than 820dp of available width. This\n         would include 7\" and 10\" devices in landscape (~960dp and ~1280dp respectively). -->\n    <dimen name=\"activity_horizontal_margin\">64dp</dimen>\n</resources>\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\napply plugin: 'io.codearte.nexus-staging'\n\nbuildscript {\n    ext.kotlin_version = '1.3.61'\n    ext.kotlin_coroutines_version = '1.3.3'\n    repositories {\n        google()\n        mavenCentral()\n        jcenter()\n        maven {\n            url 'https://dl.bintray.com/kotlin/kotlin-dev'\n            content {\n                includeGroup('org.jetbrains.dokka')\n            }\n        }\n        maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' }\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.1.3'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n        classpath 'com.dicedmelon.gradle:jacoco-android:0.1.5-SNAPSHOT'\n        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1'\n        classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.4.20.2-dev-64'\n        classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.22.0'\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        jcenter()\n        maven {\n            url 'https://dl.bintray.com/kotlin/kotlin-dev'\n            content {\n                includeGroup('org.jetbrains.dokka')\n            }\n        }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "compressor/.gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/build\n/captures"
  },
  {
    "path": "compressor/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'com.github.dcendents.android-maven'\napply plugin: 'jacoco-android'\n\next {\n    PUBLISH_GROUP_ID = 'id.zelory'\n    PUBLISH_VERSION = '3.0.1'\n    PUBLISH_ARTIFACT_ID = 'compressor'\n}\n\napply from: \"$rootDir/gradle/publish_maven.gradle\"\n\nandroid {\n    compileSdkVersion 29\n    buildToolsVersion '29.0.2'\n\n    defaultConfig {\n        minSdkVersion 14\n        targetSdkVersion 29\n        versionCode 5\n        versionName \"3.0.0\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version\"\n\n    testImplementation 'junit:junit:4.12'\n    testImplementation 'io.mockk:mockk:1.9.3'\n    testImplementation 'com.natpryce:hamkrest:1.7.0.0'\n    testImplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_version\"\n}\n"
  },
  {
    "path": "compressor/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/macbookair/Library/Android/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "compressor/src/main/AndroidManifest.xml",
    "content": "<manifest package=\"id.zelory.compressor\"/>"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/Compressor.kt",
    "content": "package id.zelory.compressor\n\nimport android.content.Context\nimport id.zelory.compressor.constraint.Compression\nimport id.zelory.compressor.constraint.default\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport kotlin.coroutines.CoroutineContext\n\n/**\n * Created on : January 22, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nobject Compressor {\n    suspend fun compress(\n            context: Context,\n            imageFile: File,\n            coroutineContext: CoroutineContext = Dispatchers.IO,\n            compressionPatch: Compression.() -> Unit = { default() }\n    ) = withContext(coroutineContext) {\n        val compression = Compression().apply(compressionPatch)\n        var result = copyToCache(context, imageFile)\n        compression.constraints.forEach { constraint ->\n            while (constraint.isSatisfied(result).not()) {\n                result = constraint.satisfy(result)\n            }\n        }\n        return@withContext result\n    }\n}"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/Util.kt",
    "content": "package id.zelory.compressor\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Matrix\nimport android.media.ExifInterface\nimport java.io.File\nimport java.io.FileOutputStream\n\n/**\n * Created on : January 24, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nprivate val separator = File.separator\n\nprivate fun cachePath(context: Context) = \"${context.cacheDir.path}${separator}compressor$separator\"\n\nfun File.compressFormat() = when (extension.toLowerCase()) {\n    \"png\" -> Bitmap.CompressFormat.PNG\n    \"webp\" -> Bitmap.CompressFormat.WEBP\n    else -> Bitmap.CompressFormat.JPEG\n}\n\nfun Bitmap.CompressFormat.extension() = when (this) {\n    Bitmap.CompressFormat.PNG -> \"png\"\n    Bitmap.CompressFormat.WEBP -> \"webp\"\n    else -> \"jpg\"\n}\n\nfun loadBitmap(imageFile: File) = BitmapFactory.decodeFile(imageFile.absolutePath).run {\n    determineImageRotation(imageFile, this)\n}\n\nfun decodeSampledBitmapFromFile(imageFile: File, reqWidth: Int, reqHeight: Int): Bitmap {\n    return BitmapFactory.Options().run {\n        inJustDecodeBounds = true\n        BitmapFactory.decodeFile(imageFile.absolutePath, this)\n        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)\n        inJustDecodeBounds = false\n        val outRatio = outWidth.toFloat() / outHeight.toFloat()\n        val reqRatio = reqWidth.toFloat() / reqHeight.toFloat()\n        if (outRatio > reqRatio) {\n            inDensity = outHeight\n            inTargetDensity = reqHeight * inSampleSize\n        } else if (outRatio <= reqRatio) {\n            inDensity = outWidth\n            inTargetDensity = reqWidth * inSampleSize\n        }\n        BitmapFactory.decodeFile(imageFile.absolutePath, this)\n    }\n}\n\nfun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {\n    // Raw height and width of image\n    val (height: Int, width: Int) = options.run { outHeight to outWidth }\n    var inSampleSize = 1\n\n    if (height > reqHeight || width > reqWidth) {\n\n        val halfHeight: Int = height / 2\n        val halfWidth: Int = width / 2\n\n        // Calculate the largest inSampleSize value that is a power of 2 and keeps both\n        // height and width larger than the requested height and width.\n        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {\n            inSampleSize *= 2\n        }\n    }\n\n    return inSampleSize\n}\n\nfun determineImageRotation(imageFile: File, bitmap: Bitmap): Bitmap {\n    val exif = ExifInterface(imageFile.absolutePath)\n    val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)\n    val matrix = Matrix()\n    when (orientation) {\n        6 -> matrix.postRotate(90f)\n        3 -> matrix.postRotate(180f)\n        8 -> matrix.postRotate(270f)\n    }\n    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)\n}\n\ninternal fun copyToCache(context: Context, imageFile: File): File {\n    return imageFile.copyTo(File(\"${cachePath(context)}${imageFile.name}\"), true)\n}\n\nfun overWrite(imageFile: File, bitmap: Bitmap, format: Bitmap.CompressFormat = imageFile.compressFormat(), quality: Int = 100): File {\n    val result = if (format == imageFile.compressFormat()) {\n        imageFile\n    } else {\n        File(\"${imageFile.absolutePath.substringBeforeLast(\".\")}.${format.extension()}\")\n    }\n    imageFile.delete()\n    saveBitmap(bitmap, result, format, quality)\n    return result\n}\n\nfun saveBitmap(bitmap: Bitmap, destination: File, format: Bitmap.CompressFormat = destination.compressFormat(), quality: Int = 100) {\n    destination.parentFile?.mkdirs()\n    var fileOutputStream: FileOutputStream? = null\n    try {\n        fileOutputStream = FileOutputStream(destination.absolutePath)\n        bitmap.compress(format, quality, fileOutputStream)\n    } finally {\n        fileOutputStream?.run {\n            flush()\n            close()\n        }\n    }\n}"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/constraint/Compression.kt",
    "content": "package id.zelory.compressor.constraint\n\nclass Compression {\n    internal val constraints: MutableList<Constraint> = mutableListOf()\n\n    fun constraint(constraint: Constraint) {\n        constraints.add(constraint)\n    }\n}"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/constraint/Constraint.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport java.io.File\n\n/**\n * Created on : January 24, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\n interface Constraint {\n    fun isSatisfied(imageFile: File): Boolean\n\n    fun satisfy(imageFile: File): File\n}"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/constraint/DefaultConstraint.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport android.graphics.Bitmap\nimport id.zelory.compressor.decodeSampledBitmapFromFile\nimport id.zelory.compressor.determineImageRotation\nimport id.zelory.compressor.overWrite\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass DefaultConstraint(\n        private val width: Int = 612,\n        private val height: Int = 816,\n        private val format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,\n        private val quality: Int = 80\n) : Constraint {\n    private var isResolved = false\n\n    override fun isSatisfied(imageFile: File): Boolean {\n        return isResolved\n    }\n\n    override fun satisfy(imageFile: File): File {\n        val result = decodeSampledBitmapFromFile(imageFile, width, height).run {\n            determineImageRotation(imageFile, this).run {\n                overWrite(imageFile, this, format, quality)\n            }\n        }\n        isResolved = true\n        return result\n    }\n}\n\nfun Compression.default(\n        width: Int = 612,\n        height: Int = 816,\n        format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,\n        quality: Int = 80\n) {\n    constraint(DefaultConstraint(width, height, format, quality))\n}"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/constraint/DestinationConstraint.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass DestinationConstraint(private val destination: File) : Constraint {\n    override fun isSatisfied(imageFile: File): Boolean {\n        return imageFile.absolutePath == destination.absolutePath\n    }\n\n    override fun satisfy(imageFile: File): File {\n        return imageFile.copyTo(destination, true)\n    }\n}\n\nfun Compression.destination(destination: File) {\n    constraint(DestinationConstraint(destination))\n}"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/constraint/FormatConstraint.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport android.graphics.Bitmap\nimport id.zelory.compressor.compressFormat\nimport id.zelory.compressor.loadBitmap\nimport id.zelory.compressor.overWrite\nimport java.io.File\n\n/**\n * Created on : January 24, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass FormatConstraint(private val format: Bitmap.CompressFormat) : Constraint {\n\n    override fun isSatisfied(imageFile: File): Boolean {\n        return format == imageFile.compressFormat()\n    }\n\n    override fun satisfy(imageFile: File): File {\n        return overWrite(imageFile, loadBitmap(imageFile), format)\n    }\n}\n\nfun Compression.format(format: Bitmap.CompressFormat) {\n    constraint(FormatConstraint(format))\n}"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/constraint/QualityConstraint.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport id.zelory.compressor.loadBitmap\nimport id.zelory.compressor.overWrite\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass QualityConstraint(private val quality: Int) : Constraint {\n    private var isResolved = false\n\n    override fun isSatisfied(imageFile: File): Boolean {\n        return isResolved\n    }\n\n    override fun satisfy(imageFile: File): File {\n        val result = overWrite(imageFile, loadBitmap(imageFile), quality = quality)\n        isResolved = true\n        return result\n    }\n}\n\nfun Compression.quality(quality: Int) {\n    constraint(QualityConstraint(quality))\n}\n\n"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/constraint/ResolutionConstraint.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport android.graphics.BitmapFactory\nimport id.zelory.compressor.calculateInSampleSize\nimport id.zelory.compressor.decodeSampledBitmapFromFile\nimport id.zelory.compressor.determineImageRotation\nimport id.zelory.compressor.overWrite\nimport java.io.File\n\n/**\n * Created on : January 24, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass ResolutionConstraint(private val width: Int, private val height: Int) : Constraint {\n\n    override fun isSatisfied(imageFile: File): Boolean {\n        return BitmapFactory.Options().run {\n            inJustDecodeBounds = true\n            BitmapFactory.decodeFile(imageFile.absolutePath, this)\n            outWidth - width <= 0 || outHeight - height <= 0\n        }\n    }\n\n    override fun satisfy(imageFile: File): File {\n        return decodeSampledBitmapFromFile(imageFile, width, height).run {\n            determineImageRotation(imageFile, this).run {\n                overWrite(imageFile, this)\n            }\n        }\n    }\n}\n\nfun Compression.resolution(width: Int, height: Int) {\n    constraint(ResolutionConstraint(width, height))\n}"
  },
  {
    "path": "compressor/src/main/java/id/zelory/compressor/constraint/SizeConstraint.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport id.zelory.compressor.loadBitmap\nimport id.zelory.compressor.overWrite\nimport java.io.File\n\n/**\n * Created on : January 24, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass SizeConstraint(\n        private val maxFileSize: Long,\n        private val stepSize: Int = 10,\n        private val maxIteration: Int = 10,\n        private val minQuality: Int = 10\n) : Constraint {\n    private var iteration: Int = 0\n\n    override fun isSatisfied(imageFile: File): Boolean {\n        return imageFile.length() <= maxFileSize || iteration >= maxIteration\n    }\n\n    override fun satisfy(imageFile: File): File {\n        iteration++\n        val quality = (100 - iteration * stepSize).takeIf { it >= minQuality } ?: minQuality\n        return overWrite(imageFile, loadBitmap(imageFile), quality = quality)\n    }\n}\n\nfun Compression.size(maxFileSize: Long, stepSize: Int = 10, maxIteration: Int = 10) {\n    constraint(SizeConstraint(maxFileSize, stepSize, maxIteration))\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/CompressorTest.kt",
    "content": "package id.zelory.compressor\n\nimport android.graphics.Bitmap\nimport id.zelory.compressor.constraint.DefaultConstraint\nimport id.zelory.compressor.constraint.FormatConstraint\nimport id.zelory.compressor.constraint.QualityConstraint\nimport id.zelory.compressor.constraint.ResolutionConstraint\nimport id.zelory.compressor.constraint.format\nimport id.zelory.compressor.constraint.quality\nimport id.zelory.compressor.constraint.resolution\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.mockkConstructor\nimport io.mockk.mockkStatic\nimport io.mockk.verify\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.test.TestCoroutineDispatcher\nimport kotlinx.coroutines.test.resetMain\nimport kotlinx.coroutines.test.runBlockingTest\nimport kotlinx.coroutines.test.setMain\nimport org.junit.After\nimport org.junit.Before\nimport org.junit.Test\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass CompressorTest {\n\n    private val testDispatcher = TestCoroutineDispatcher()\n\n    @Before\n    fun setup() {\n        Dispatchers.setMain(testDispatcher)\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { copyToCache(any(), any()) } returns mockk(relaxed = true)\n    }\n\n    @After\n    fun cleanUp() {\n        Dispatchers.resetMain()\n        testDispatcher.cleanupTestCoroutines()\n    }\n\n    @Test\n    fun `compress with default specs should execute default constraint`() = testDispatcher.runBlockingTest {\n        // Given\n        mockkConstructor(DefaultConstraint::class)\n        var executedConstraint = 0\n        every { anyConstructed<DefaultConstraint>().isSatisfied(any()) } answers {\n            executedConstraint > 0\n        }\n        every { anyConstructed<DefaultConstraint>().satisfy(any()) } answers {\n            executedConstraint++\n            mockk(relaxed = true)\n        }\n\n        // When\n        Compressor.compress(mockk(relaxed = true), mockk(relaxed = true), testDispatcher)\n\n        // Then\n        verify {\n            anyConstructed<DefaultConstraint>().isSatisfied(any())\n            anyConstructed<DefaultConstraint>().satisfy(any())\n        }\n    }\n\n    @Test\n    fun `compress with custom specs should execute all constraint provided`() = testDispatcher.runBlockingTest {\n        // Given\n        mockkConstructor(ResolutionConstraint::class)\n        mockkConstructor(QualityConstraint::class)\n        mockkConstructor(FormatConstraint::class)\n\n        var executedConstraint = 0\n        every { anyConstructed<ResolutionConstraint>().isSatisfied(any()) } answers {\n            executedConstraint > 0\n        }\n        every { anyConstructed<ResolutionConstraint>().satisfy(any()) } answers {\n            executedConstraint++\n            mockk(relaxed = true)\n        }\n\n        every { anyConstructed<QualityConstraint>().isSatisfied(any()) } answers {\n            executedConstraint > 1\n        }\n        every { anyConstructed<QualityConstraint>().satisfy(any()) } answers {\n            executedConstraint++\n            mockk(relaxed = true)\n        }\n\n        every { anyConstructed<FormatConstraint>().isSatisfied(any()) } answers {\n            executedConstraint > 2\n        }\n        every { anyConstructed<FormatConstraint>().satisfy(any()) } answers {\n            executedConstraint++\n            mockk(relaxed = true)\n        }\n\n        // When\n        Compressor.compress(mockk(relaxed = true), mockk(relaxed = true), testDispatcher) {\n            resolution(100, 100)\n            quality(75)\n            format(Bitmap.CompressFormat.PNG)\n        }\n\n        // Then\n        verify {\n            anyConstructed<ResolutionConstraint>().isSatisfied(any())\n            anyConstructed<ResolutionConstraint>().satisfy(any())\n            anyConstructed<QualityConstraint>().isSatisfied(any())\n            anyConstructed<QualityConstraint>().satisfy(any())\n            anyConstructed<FormatConstraint>().isSatisfied(any())\n            anyConstructed<FormatConstraint>().satisfy(any())\n        }\n    }\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/UtilTest.kt",
    "content": "package id.zelory.compressor\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport com.natpryce.hamkrest.assertion.assertThat\nimport com.natpryce.hamkrest.equalTo\nimport io.mockk.Runs\nimport io.mockk.every\nimport io.mockk.just\nimport io.mockk.mockk\nimport io.mockk.mockkStatic\nimport io.mockk.unmockkAll\nimport io.mockk.verify\nimport org.junit.After\nimport org.junit.Test\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass UtilTest {\n\n    @After\n    fun teardown() {\n        unmockkAll()\n    }\n\n    @Test\n    fun `get compress format from file should return correct format`() {\n        assertThat(File(\"a_file.png\").compressFormat(), equalTo(Bitmap.CompressFormat.PNG))\n        assertThat(File(\"a_file.webp\").compressFormat(), equalTo(Bitmap.CompressFormat.WEBP))\n        assertThat(File(\"a_file.jpg\").compressFormat(), equalTo(Bitmap.CompressFormat.JPEG))\n        assertThat(File(\"a_file.jpeg\").compressFormat(), equalTo(Bitmap.CompressFormat.JPEG))\n    }\n\n    @Test\n    fun `get extension from compress format should return correct extension`() {\n        assertThat(Bitmap.CompressFormat.PNG.extension(), equalTo(\"png\"))\n        assertThat(Bitmap.CompressFormat.WEBP.extension(), equalTo(\"webp\"))\n        assertThat(Bitmap.CompressFormat.JPEG.extension(), equalTo(\"jpg\"))\n    }\n\n    @Test\n    fun `load bitmap should determine image rotation`() {\n        // Given\n        mockkStatic(BitmapFactory::class)\n        every { BitmapFactory.decodeFile(any()) } returns mockk()\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { determineImageRotation(any(), any()) } returns mockk()\n\n        // When\n        loadBitmap(mockk(relaxed = true))\n\n        // Then\n        verify { determineImageRotation(any(), any()) }\n    }\n\n    @Test\n    fun `decode sampled bitmap should decode with subsampling`() {\n        // Given\n        mockkStatic(BitmapFactory::class)\n        every { BitmapFactory.decodeFile(any(), any()) } returns mockk()\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        val inSampleSize = 2\n        every { calculateInSampleSize(any(), any(), any()) } returns inSampleSize\n\n        // When\n        decodeSampledBitmapFromFile(mockk(relaxed = true), 100, 100)\n\n        // Then\n        verify {\n            BitmapFactory.decodeFile(any(), match {\n                it.inSampleSize == inSampleSize\n            })\n        }\n    }\n\n    @Test\n    fun `when request half of resolution, it should return 2 in sample size`() {\n        // Given\n        val options = BitmapFactory.Options().apply {\n            outWidth = 800\n            outHeight = 800\n        }\n\n        // When + Then\n        assertThat(calculateInSampleSize(options, 400, 400), equalTo(2))\n    }\n\n    @Test\n    fun `when resolution requested greater than actual resolution, it should return 1 in sample size`() {\n        // Given\n        val options = BitmapFactory.Options().apply {\n            outWidth = 800\n            outHeight = 800\n        }\n\n        // When + Then\n        assertThat(calculateInSampleSize(options, 1000, 1000), equalTo(1))\n    }\n\n    @Test\n    fun `when partial resolution requested greater than actual resolution, it should return 1 in sample size`() {\n        // Given\n        val options = BitmapFactory.Options().apply {\n            outWidth = 800\n            outHeight = 800\n        }\n\n        // When + Then\n        assertThat(calculateInSampleSize(options, 1000, 500), equalTo(1))\n    }\n\n    @Test\n    fun `when resolution requested less than actual resolution but greater than of half it, it should return 1 in sample size`() {\n        // Given\n        val options = BitmapFactory.Options().apply {\n            outWidth = 800\n            outHeight = 800\n        }\n\n        // When + Then\n        assertThat(calculateInSampleSize(options, 500, 500), equalTo(1))\n    }\n\n    @Test\n    fun `when request 25% of resolution, it should return 4 in sample size`() {\n        // Given\n        val options = BitmapFactory.Options().apply {\n            outWidth = 800\n            outHeight = 800\n        }\n\n        // When + Then\n        assertThat(calculateInSampleSize(options, 200, 200), equalTo(4))\n    }\n\n    @Test\n    fun `when width 25% and height 50% of resolution, it should return min sample size (height)`() {\n        // Given\n        val options = BitmapFactory.Options().apply {\n            outWidth = 800\n            outHeight = 800\n        }\n\n        // When + Then\n        assertThat(calculateInSampleSize(options, 200, 400), equalTo(2))\n    }\n\n    @Test\n    fun `copy to cache should copy file to right folder`() {\n        // Given\n        val context = mockk<Context>(relaxed = true)\n        every { context.cacheDir.path } returns \"folder/\"\n\n        mockkStatic(\"kotlin.io.FilesKt__UtilsKt\")\n        every { any<File>().copyTo(any(), any(), any()) } returns mockk(relaxed = true)\n\n        val source = File(\"image.jpg\")\n\n        // When\n        copyToCache(context, File(\"image.jpg\"))\n\n        // Then\n        verify {\n            source.copyTo(File(\"folder/compressor/image.jpg\"), true, any())\n        }\n    }\n\n    @Test\n    fun `overwrite should delete old file and save new bitmap`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { saveBitmap(any(), any(), any(), any()) } just Runs\n\n        val imageFile = mockk<File>(relaxed = true)\n        val bitmap = mockk<Bitmap>(relaxed = true)\n\n        // When\n        overWrite(imageFile, bitmap)\n\n        // Then\n        verify {\n            imageFile.delete()\n            saveBitmap(bitmap, imageFile, any(), any())\n        }\n    }\n\n    @Test\n    fun `overwrite with different format should save image with new format extension`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { saveBitmap(any(), any(), any(), any()) } just Runs\n\n        val imageFile = File(\"image.jpg\")\n        val bitmap = mockk<Bitmap>(relaxed = true)\n\n        // When\n        val result = overWrite(imageFile, bitmap, Bitmap.CompressFormat.PNG)\n\n        // Then\n        assertThat(result.extension, equalTo(\"png\"))\n    }\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/constraint/CompressionTest.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport com.natpryce.hamkrest.assertion.assertThat\nimport com.natpryce.hamkrest.equalTo\nimport io.mockk.mockk\nimport org.junit.Test\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass CompressionTest {\n    @Test\n    fun `add constraint should save it to constraint list`() {\n        // Given\n        val compression = Compression()\n\n        // When\n        compression.constraint(mockk())\n        compression.constraint(mockk())\n\n        // Then\n        assertThat(compression.constraints.size, equalTo(2))\n    }\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/constraint/DefaultConstraintTest.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport android.graphics.Bitmap\nimport com.natpryce.hamkrest.assertion.assertThat\nimport com.natpryce.hamkrest.equalTo\nimport com.natpryce.hamkrest.isA\nimport id.zelory.compressor.decodeSampledBitmapFromFile\nimport id.zelory.compressor.determineImageRotation\nimport id.zelory.compressor.overWrite\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.mockkStatic\nimport io.mockk.unmockkAll\nimport io.mockk.verify\nimport org.junit.After\nimport org.junit.Test\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass DefaultConstraintTest {\n\n    @After\n    fun teardown() {\n        unmockkAll()\n    }\n\n    @Test\n    fun `when satisfy function not yet invoked, constraint should not satisfied`() {\n        // Given\n        val constraint = DefaultConstraint()\n\n        // When + Then\n        assertThat(constraint.isSatisfied(mockk()), equalTo(false))\n    }\n\n    @Test\n    fun `when satisfy function is invoked, constraint should satisfied`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { decodeSampledBitmapFromFile(any(), any(), any()) } returns mockk(relaxed = true)\n        every { determineImageRotation(any(), any()) } returns mockk(relaxed = true)\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val constraint = DefaultConstraint()\n\n        // When\n        constraint.satisfy(mockk(relaxed = true))\n\n        // Then\n        assertThat(constraint.isSatisfied(mockk()), equalTo(true))\n    }\n\n    @Test\n    fun `when trying satisfy constraint, it should subsampling image and overwrite file`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n\n        val sampledBitmap = mockk<Bitmap>(relaxed = true)\n        every { decodeSampledBitmapFromFile(any(), any(), any()) } returns sampledBitmap\n\n        val rotatedBitmap = mockk<Bitmap>(relaxed = true)\n        every { determineImageRotation(any(), any()) } returns rotatedBitmap\n\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val imageFile = mockk<File>(relaxed = true)\n        val (width, height) = (1280 to 720)\n        val format = Bitmap.CompressFormat.JPEG\n        val quality = 80\n        val constraint = DefaultConstraint(width, height, format, quality)\n\n        // When\n        constraint.satisfy(imageFile)\n\n        // Then\n        verify {\n            decodeSampledBitmapFromFile(imageFile, width, height)\n            determineImageRotation(imageFile, sampledBitmap)\n            overWrite(imageFile, rotatedBitmap, format, quality)\n        }\n    }\n\n    @Test\n    fun `verify extension`() {\n        // Given\n        val compression = Compression()\n\n        // When\n        compression.default()\n\n        // Then\n        assertThat(compression.constraints.first(), isA<DefaultConstraint>())\n    }\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/constraint/DestinationConstraintTest.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport com.natpryce.hamkrest.assertion.assertThat\nimport com.natpryce.hamkrest.equalTo\nimport com.natpryce.hamkrest.isA\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.mockkStatic\nimport io.mockk.unmockkAll\nimport io.mockk.verify\nimport org.junit.After\nimport org.junit.Test\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass DestinationConstraintTest {\n\n    @After\n    fun teardown() {\n        unmockkAll()\n    }\n\n    @Test\n    fun `when destination is not equal with image file, constraint should not satisfied`() {\n        // Given\n        val constraint = DestinationConstraint(File(\"a_file.webp\"))\n\n        // When + Then\n        assertThat(constraint.isSatisfied(File(\"another_file.png\")), equalTo(false))\n    }\n\n    @Test\n    fun `when destination is equal with image file, constraint should satisfied`() {\n        // Given\n        val constraint = DestinationConstraint(File(\"a_file.jpg\"))\n\n        // When + Then\n        assertThat(constraint.isSatisfied(File(\"a_file.jpg\")), equalTo(true))\n    }\n\n    @Test\n    fun `when trying satisfy constraint, it should copy image to destination`() {\n        // Given\n        mockkStatic(\"kotlin.io.FilesKt__UtilsKt\")\n        every { any<File>().copyTo(any(), any(), any()) } returns mockk(relaxed = true)\n\n        val imageFile = File(\"source.jpg\")\n        val destination = File(\"destination.jpg\")\n        val constraint = DestinationConstraint(destination)\n\n        // When\n        constraint.satisfy(imageFile)\n\n        // Then\n        verify { imageFile.copyTo(destination, true, any()) }\n    }\n\n    @Test\n    fun `verify extension`() {\n        // Given\n        val compression = Compression()\n\n        // When\n        compression.destination(mockk())\n\n        // Then\n        assertThat(compression.constraints.first(), isA<DestinationConstraint>())\n    }\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/constraint/FormatConstraintTest.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport android.graphics.Bitmap\nimport com.natpryce.hamkrest.assertion.assertThat\nimport com.natpryce.hamkrest.equalTo\nimport com.natpryce.hamkrest.isA\nimport id.zelory.compressor.loadBitmap\nimport id.zelory.compressor.overWrite\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.mockkStatic\nimport io.mockk.unmockkAll\nimport io.mockk.verify\nimport org.junit.After\nimport org.junit.Test\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass FormatConstraintTest {\n\n    @After\n    fun teardown() {\n        unmockkAll()\n    }\n\n    @Test\n    fun `when extension is not equal with format, constraint should not satisfied`() {\n        // Given\n        val constraint = FormatConstraint(Bitmap.CompressFormat.JPEG)\n\n        // When + Then\n        assertThat(constraint.isSatisfied(File(\"a_file.webp\")), equalTo(false))\n    }\n\n    @Test\n    fun `when extension is equal with format, constraint should satisfied`() {\n        // Given\n        val constraint = FormatConstraint(Bitmap.CompressFormat.WEBP)\n\n        // When + Then\n        assertThat(constraint.isSatisfied(File(\"a_file.webp\")), equalTo(true))\n    }\n\n    @Test\n    fun `when trying satisfy constraint, it should save image with selected format`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { loadBitmap(any()) } returns mockk()\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val imageFile = mockk<File>()\n        val format = Bitmap.CompressFormat.PNG\n        val constraint = FormatConstraint(format)\n\n        // When\n        constraint.satisfy(imageFile)\n\n        // Then\n        verify { overWrite(imageFile, any(), format, any()) }\n    }\n\n    @Test\n    fun `verify extension`() {\n        // Given\n        val compression = Compression()\n\n        // When\n        compression.format(Bitmap.CompressFormat.PNG)\n\n        // Then\n        assertThat(compression.constraints.first(), isA<FormatConstraint>())\n    }\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/constraint/QualityConstraintTest.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport com.natpryce.hamkrest.assertion.assertThat\nimport com.natpryce.hamkrest.equalTo\nimport com.natpryce.hamkrest.isA\nimport id.zelory.compressor.loadBitmap\nimport id.zelory.compressor.overWrite\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.mockkStatic\nimport io.mockk.unmockkAll\nimport io.mockk.verify\nimport org.junit.After\nimport org.junit.Test\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass QualityConstraintTest {\n\n    @After\n    fun teardown() {\n        unmockkAll()\n    }\n\n    @Test\n    fun `when satisfy function not yet invoked, constraint should not satisfied`() {\n        // Given\n        val constraint = QualityConstraint(mockk(relaxed = true))\n\n        // When + Then\n        assertThat(constraint.isSatisfied(mockk()), equalTo(false))\n    }\n\n    @Test\n    fun `when satisfy function is invoked, constraint should satisfied`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { loadBitmap(any()) } returns mockk()\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val constraint = QualityConstraint(mockk(relaxed = true))\n\n        // When\n        constraint.satisfy(mockk(relaxed = true))\n\n        // Then\n        assertThat(constraint.isSatisfied(mockk()), equalTo(true))\n    }\n\n    @Test\n    fun `when trying satisfy constraint, it should save image with provided quality`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { loadBitmap(any()) } returns mockk()\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val imageFile = mockk<File>(relaxed = true)\n        val quality = 75\n        val constraint = QualityConstraint(quality)\n\n        // When\n        constraint.satisfy(imageFile)\n\n        // Then\n        verify { overWrite(imageFile, any(), any(), quality) }\n    }\n\n    @Test\n    fun `verify extension`() {\n        // Given\n        val compression = Compression()\n\n        // When\n        compression.quality(90)\n\n        // Then\n        assertThat(compression.constraints.first(), isA<QualityConstraint>())\n    }\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/constraint/ResolutionConstraintTest.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport com.natpryce.hamkrest.assertion.assertThat\nimport com.natpryce.hamkrest.equalTo\nimport com.natpryce.hamkrest.isA\nimport id.zelory.compressor.calculateInSampleSize\nimport id.zelory.compressor.decodeSampledBitmapFromFile\nimport id.zelory.compressor.determineImageRotation\nimport id.zelory.compressor.overWrite\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.mockkStatic\nimport io.mockk.unmockkAll\nimport io.mockk.verify\nimport org.junit.After\nimport org.junit.Test\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass ResolutionConstraintTest {\n\n    @After\n    fun teardown() {\n        unmockkAll()\n    }\n\n    @Test\n    fun `when sampled size is greater than 1, constraint should not satisfied`() {\n        // Given\n        mockkStatic(BitmapFactory::class)\n        every { BitmapFactory.decodeFile(any(), any()) } returns mockk()\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { calculateInSampleSize(any(), any(), any()) } returns 2\n\n        val constraint = ResolutionConstraint(100, 100)\n\n        // When + Then\n        assertThat(constraint.isSatisfied(mockk(relaxed = true)), equalTo(false))\n    }\n\n    @Test\n    fun `when sampled size is equal 1, constraint should satisfied`() {\n        // Given\n        mockkStatic(BitmapFactory::class)\n        every { BitmapFactory.decodeFile(any(), any()) } returns mockk()\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { calculateInSampleSize(any(), any(), any()) } returns 1\n\n        val constraint = ResolutionConstraint(100, 100)\n\n        // When + Then\n        assertThat(constraint.isSatisfied(mockk(relaxed = true)), equalTo(true))\n    }\n\n    @Test\n    fun `when trying satisfy constraint, it should subsampling image and overwrite file`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n\n        val sampledBitmap = mockk<Bitmap>(relaxed = true)\n        every { decodeSampledBitmapFromFile(any(), any(), any()) } returns sampledBitmap\n\n        val rotatedBitmap = mockk<Bitmap>(relaxed = true)\n        every { determineImageRotation(any(), any()) } returns rotatedBitmap\n\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val imageFile = mockk<File>(relaxed = true)\n        val (width, height) = (1280 to 720)\n        val constraint = ResolutionConstraint(width, height)\n\n        // When\n        constraint.satisfy(imageFile)\n\n        // Then\n        verify {\n            decodeSampledBitmapFromFile(imageFile, width, height)\n            determineImageRotation(imageFile, sampledBitmap)\n            overWrite(imageFile, rotatedBitmap, any(), any())\n        }\n    }\n\n    @Test\n    fun `verify extension`() {\n        // Given\n        val compression = Compression()\n\n        // When\n        compression.resolution(100, 100)\n\n        // Then\n        assertThat(compression.constraints.first(), isA<ResolutionConstraint>())\n    }\n}"
  },
  {
    "path": "compressor/src/test/java/id/zelory/compressor/constraint/SizeConstraintTest.kt",
    "content": "package id.zelory.compressor.constraint\n\nimport com.natpryce.hamkrest.assertion.assertThat\nimport com.natpryce.hamkrest.equalTo\nimport com.natpryce.hamkrest.isA\nimport id.zelory.compressor.loadBitmap\nimport id.zelory.compressor.overWrite\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.mockkStatic\nimport io.mockk.unmockkAll\nimport io.mockk.verify\nimport org.junit.After\nimport org.junit.Test\nimport java.io.File\n\n/**\n * Created on : January 25, 2020\n * Author     : zetbaitsu\n * Name       : Zetra\n * GitHub     : https://github.com/zetbaitsu\n */\nclass SizeConstraintTest {\n\n    @After\n    fun teardown() {\n        unmockkAll()\n    }\n\n    @Test\n    fun `when file size greater than max file size, constraint should not satisfied`() {\n        // Given\n        val imageFile = mockk<File>(relaxed = true)\n        every { imageFile.length() } returns 2000\n        val constraint = SizeConstraint(1000)\n\n        // When + Then\n        assertThat(constraint.isSatisfied(imageFile), equalTo(false))\n    }\n\n    @Test\n    fun `when file size equal to max file size, constraint should satisfied`() {\n        // Given\n        val imageFile = mockk<File>(relaxed = true)\n        every { imageFile.length() } returns 1000\n        val constraint = SizeConstraint(1000)\n\n        // When + Then\n        assertThat(constraint.isSatisfied(imageFile), equalTo(true))\n    }\n\n    @Test\n    fun `when file size less than max file size, constraint should satisfied`() {\n        // Given\n        val imageFile = mockk<File>(relaxed = true)\n        every { imageFile.length() } returns 900\n        val constraint = SizeConstraint(1000)\n\n        // When + Then\n        assertThat(constraint.isSatisfied(imageFile), equalTo(true))\n    }\n\n    @Test\n    fun `when iteration less than max iteration, constraint should not satisfied`() {\n        // Given\n        val imageFile = mockk<File>(relaxed = true)\n        every { imageFile.length() } returns 2000\n\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { loadBitmap(any()) } returns mockk()\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val constraint = SizeConstraint(1000, maxIteration = 5)\n\n        // When\n        constraint.satisfy(imageFile)\n\n        // Then\n        assertThat(constraint.isSatisfied(imageFile), equalTo(false))\n    }\n\n    @Test\n    fun `when iteration equal to max iteration, constraint should satisfied`() {\n        // Given\n        val imageFile = mockk<File>(relaxed = true)\n        every { imageFile.length() } returns 2000\n\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { loadBitmap(any()) } returns mockk()\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val constraint = SizeConstraint(1000, maxIteration = 5)\n\n        // When\n        repeat(5) {\n            constraint.satisfy(imageFile)\n        }\n\n        // Then\n        assertThat(constraint.isSatisfied(imageFile), equalTo(true))\n    }\n\n    @Test\n    fun `when trying satisfy constraint, it should save image with calculated quality`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { loadBitmap(any()) } returns mockk()\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val imageFile = mockk<File>(relaxed = true)\n        val stepSize = 10\n        val quality = 100 - stepSize\n        val constraint = SizeConstraint(200, stepSize = stepSize)\n\n        // When\n        constraint.satisfy(imageFile)\n\n        // Then\n        verify { overWrite(imageFile, any(), any(), quality) }\n    }\n\n    @Test\n    fun `when trying satisfy constraint but calculated quality less than min quality, it should use min quality`() {\n        // Given\n        mockkStatic(\"id.zelory.compressor.UtilKt\")\n        every { loadBitmap(any()) } returns mockk()\n        every { overWrite(any(), any(), any(), any()) } returns mockk()\n\n        val imageFile = mockk<File>(relaxed = true)\n        val stepSize = 50\n        val minQuality = 80\n        val constraint = SizeConstraint(200, stepSize, minQuality = minQuality)\n\n        // When\n        constraint.satisfy(imageFile)\n\n        // Then\n        verify { overWrite(imageFile, any(), any(), minQuality) }\n    }\n\n    @Test\n    fun `verify extension`() {\n        // Given\n        val compression = Compression()\n\n        // When\n        compression.size(9000)\n\n        // Then\n        assertThat(compression.constraints.first(), isA<SizeConstraint>())\n    }\n}"
  },
  {
    "path": "gradle/publish_maven.gradle",
    "content": "apply plugin: 'maven-publish'\napply plugin: 'signing'\napply plugin: 'org.jetbrains.dokka'\n\ntask androidSourcesJar(type: Jar) {\n    archiveClassifier.set('sources')\n    if (project.plugins.findPlugin(\"com.android.library\")) {\n        from android.sourceSets.main.java.srcDirs\n        from android.sourceSets.main.kotlin.srcDirs\n    } else {\n        from sourceSets.main.java.srcDirs\n        from sourceSets.main.kotlin.srcDirs\n    }\n}\n\ntasks.withType(dokkaHtmlPartial.getClass()).configureEach {\n    pluginsMapConfiguration.set(\n            [\"org.jetbrains.dokka.base.DokkaBase\": \"\"\"{ \"separateInheritedMembers\": true}\"\"\"]\n    )\n}\n\ntask javadocJar(type: Jar, dependsOn: dokkaJavadoc) {\n    archiveClassifier.set('javadoc')\n    from dokkaJavadoc.outputDirectory\n}\n\nartifacts {\n    archives androidSourcesJar\n    archives javadocJar\n}\n\n\ngroup = PUBLISH_GROUP_ID\nversion = PUBLISH_VERSION\n\next[\"signing.keyId\"] = ''\next[\"signing.password\"] = ''\next[\"signing.secretKeyRingFile\"] = ''\next[\"ossrhUsername\"] = ''\next[\"ossrhPassword\"] = ''\next[\"sonatypeStagingProfileId\"] = ''\n\nFile secretPropsFile = project.rootProject.file('local.properties')\nif (secretPropsFile.exists()) {\n    Properties p = new Properties()\n    p.load(new FileInputStream(secretPropsFile))\n    p.each { name, value ->\n        ext[name] = value\n    }\n} else {\n    ext[\"signing.keyId\"] = System.getenv('SIGNING_KEY_ID')\n    ext[\"signing.password\"] = System.getenv('SIGNING_PASSWORD')\n    ext[\"signing.secretKeyRingFile\"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')\n    ext[\"ossrhUsername\"] = System.getenv('OSSRH_USERNAME')\n    ext[\"ossrhPassword\"] = System.getenv('OSSRH_PASSWORD')\n    ext[\"sonatypeStagingProfileId\"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')\n}\n\npublishing {\n    publications {\n        release(MavenPublication) {\n            groupId PUBLISH_GROUP_ID\n            artifactId PUBLISH_ARTIFACT_ID\n            version PUBLISH_VERSION\n            if (project.plugins.findPlugin(\"com.android.library\")) {\n                artifact(\"$buildDir/outputs/aar/${project.getName()}-release.aar\")\n            } else {\n                artifact(\"$buildDir/libs/${project.getName()}-${version}.jar\")\n            }\n\n            artifact androidSourcesJar\n            artifact javadocJar\n\n            pom {\n                name = PUBLISH_ARTIFACT_ID\n                description = 'An android image compressor library'\n                url = 'https://github.com/zetbaitsu/Compressor'\n                licenses {\n                    license {\n                        name = 'The Apache Software License, Version 2.0'\n                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'\n                    }\n                }\n                developers {\n                    developer {\n                        id = 'zetbaitsu'\n                        name = 'Zetra'\n                        email = 'zetbaitsu@gmail.com'\n                    }\n                }\n                scm {\n                    connection = 'scm:git:github.com/zetbaitsu/Compressor.git'\n                    developerConnection = 'scm:git:ssh://github.com/zetbaitsu/Compressor.git'\n                    url = 'https://github.com/zetbaitsu/Compressor/tree/main'\n                }\n                withXml {\n                    def dependenciesNode = asNode().appendNode('dependencies')\n\n                    project.configurations.implementation.allDependencies.each {\n                        def dependencyNode = dependenciesNode.appendNode('dependency')\n                        dependencyNode.appendNode('groupId', it.group)\n                        dependencyNode.appendNode('artifactId', it.name)\n                        dependencyNode.appendNode('version', it.version)\n                    }\n                }\n            }\n        }\n    }\n    repositories {\n        maven {\n            name = \"sonatype\"\n\n            def releasesRepoUrl = \"https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/\"\n            def snapshotsRepoUrl = \"https://s01.oss.sonatype.org/content/repositories/snapshots/\"\n            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl\n\n            credentials {\n                username ossrhUsername\n                password ossrhPassword\n            }\n        }\n    }\n}\n\nnexusStaging {\n    packageGroup = PUBLISH_GROUP_ID\n    stagingProfileId = sonatypeStagingProfileId\n    username = ossrhUsername\n    password = ossrhPassword\n}\n\nsigning {\n    sign publishing.publications\n}"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Sun Mar 21 19:52:31 WIB 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-bin.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx10248m -XX:MaxPermSize=256m\n# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\nandroid.useAndroidX=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windowz variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\ngoto execute\r\n\r\n:4NT_args\r\n@rem Get arguments from the 4NT Shell from JP Software\r\nset CMD_LINE_ARGS=%$\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':compressor'\n"
  }
]