├── .github ├── FUNDING.yml └── workflows │ ├── iOS.yml │ ├── tvOS.yml │ ├── visionOS.yml │ ├── watchOS.yml │ ├── android.yml │ ├── Tests.yml │ └── Documentation.yml ├── tests ├── unit │ ├── result.swift │ └── main.swift ├── integration │ ├── result.swift │ ├── encode │ │ ├── karlie-kloss-1.rgb │ │ └── attribution.md │ ├── decode │ │ ├── color-sequential-1.jpg │ │ ├── color-sequential-2.jpg │ │ ├── color-sequential-3.jpg │ │ ├── color-sequential-4.jpg │ │ ├── color-progressive-1.jpg │ │ ├── color-progressive-2.jpg │ │ ├── color-progressive-3.jpg │ │ ├── color-progressive-4.jpg │ │ ├── grayscale-progressive-1.jpg │ │ ├── grayscale-progressive-2.jpg │ │ ├── grayscale-sequential-1.jpg │ │ ├── grayscale-sequential-2.jpg │ │ ├── color-progressive-restart.jpg │ │ ├── color-sequential-restart.jpg │ │ ├── grayscale-sequential-restart.jpg │ │ ├── grayscale-progressive-restart.jpg │ │ └── attribution.md │ ├── main.swift │ └── tests.swift ├── regression │ ├── result.swift │ ├── gold │ │ ├── attribution.md │ │ ├── color-progressive-1.jpg │ │ ├── color-progressive-2.jpg │ │ ├── color-progressive-3.jpg │ │ ├── color-progressive-4.jpg │ │ ├── color-sequential-1.jpg │ │ ├── color-sequential-2.jpg │ │ ├── color-sequential-3.jpg │ │ ├── color-sequential-4.jpg │ │ ├── grayscale-progressive-1.jpg │ │ ├── grayscale-progressive-2.jpg │ │ ├── grayscale-sequential-1.jpg │ │ ├── grayscale-sequential-2.jpg │ │ ├── color-progressive-1.jpg.rgb │ │ ├── color-progressive-1.jpg.ycc │ │ ├── color-progressive-2.jpg.rgb │ │ ├── color-progressive-2.jpg.ycc │ │ ├── color-progressive-3.jpg.rgb │ │ ├── color-progressive-3.jpg.ycc │ │ ├── color-progressive-4.jpg.rgb │ │ ├── color-progressive-4.jpg.ycc │ │ ├── color-sequential-1.jpg.rgb │ │ ├── color-sequential-1.jpg.ycc │ │ ├── color-sequential-2.jpg.rgb │ │ ├── color-sequential-2.jpg.ycc │ │ ├── color-sequential-3.jpg.rgb │ │ ├── color-sequential-3.jpg.ycc │ │ ├── color-sequential-4.jpg.rgb │ │ ├── color-sequential-4.jpg.ycc │ │ ├── grayscale-sequential-1.jpg.rgb │ │ ├── grayscale-sequential-1.jpg.ycc │ │ ├── grayscale-sequential-2.jpg.rgb │ │ ├── grayscale-sequential-2.jpg.ycc │ │ ├── grayscale-progressive-1.jpg.rgb │ │ ├── grayscale-progressive-1.jpg.ycc │ │ ├── grayscale-progressive-2.jpg.rgb │ │ └── grayscale-progressive-2.jpg.ycc │ ├── main.swift │ └── tests.swift ├── common │ └── result.swift ├── compare │ └── main.swift └── fuzz │ └── main.swift ├── .mailmap ├── Scripts └── TestAll ├── examples ├── custom-color │ ├── output.jpg │ ├── output.jpg.rgb │ ├── output.jpg.rgb-8.png │ ├── output.jpg.rgb-16.png │ └── output.jpg.rgb-difference.png ├── recompress │ ├── original.jpg │ ├── recompressed-full-cycle.jpg │ └── recompressed-requantized.jpg ├── in-memory │ ├── karlie-2011.jpg │ ├── karlie-2011.jpg.jpg │ ├── karlie-2011.jpg.rgb │ └── karlie-2011.jpg.rgb.png ├── decode-advanced │ ├── karlie-2019.jpg │ ├── karlie-2019.jpg.rgb │ ├── karlie-2019.jpg.rgb.png │ ├── karlie-2019.jpg-0.640x432.gray │ ├── karlie-2019.jpg-1.320x216.gray │ ├── karlie-2019.jpg-2.320x216.gray │ ├── karlie-2019.jpg-0.640x432.gray.png │ ├── karlie-2019.jpg-1.320x216.gray.png │ └── karlie-2019.jpg-2.320x216.gray.png ├── decode-basic │ ├── karlie-kwk-2019.jpg │ ├── karlie-kwk-2019.jpg.rgb │ └── karlie-kwk-2019.jpg.rgb.png ├── rotate │ ├── karlie-kwk-wwdc-2017.jpg │ ├── karlie-kwk-wwdc-2017-ii.jpg │ ├── karlie-kwk-wwdc-2017-iii.jpg │ └── karlie-kwk-wwdc-2017-iv.jpg ├── decode-online │ ├── karlie-oscars-2017.jpg │ ├── karlie-oscars-2017.jpg-0.rgb │ ├── karlie-oscars-2017.jpg-1.rgb │ ├── karlie-oscars-2017.jpg-2.rgb │ ├── karlie-oscars-2017.jpg-3.rgb │ ├── karlie-oscars-2017.jpg-4.rgb │ ├── karlie-oscars-2017.jpg-5.rgb │ ├── karlie-oscars-2017.jpg-6.rgb │ ├── karlie-oscars-2017.jpg-7.rgb │ ├── karlie-oscars-2017.jpg-8.rgb │ ├── karlie-oscars-2017.jpg-9.rgb │ ├── karlie-oscars-2017.jpg-0.rgb.png │ ├── karlie-oscars-2017.jpg-1.rgb.png │ ├── karlie-oscars-2017.jpg-2.rgb.png │ ├── karlie-oscars-2017.jpg-3.rgb.png │ ├── karlie-oscars-2017.jpg-4.rgb.png │ ├── karlie-oscars-2017.jpg-5.rgb.png │ ├── karlie-oscars-2017.jpg-6.rgb.png │ ├── karlie-oscars-2017.jpg-7.rgb.png │ ├── karlie-oscars-2017.jpg-8.rgb.png │ ├── karlie-oscars-2017.jpg-9.rgb.png │ ├── karlie-oscars-2017.jpg-difference-0.rgb │ ├── karlie-oscars-2017.jpg-difference-1.rgb │ ├── karlie-oscars-2017.jpg-difference-2.rgb │ ├── karlie-oscars-2017.jpg-difference-3.rgb │ ├── karlie-oscars-2017.jpg-difference-4.rgb │ ├── karlie-oscars-2017.jpg-difference-5.rgb │ ├── karlie-oscars-2017.jpg-difference-6.rgb │ ├── karlie-oscars-2017.jpg-difference-7.rgb │ ├── karlie-oscars-2017.jpg-difference-8.rgb │ ├── karlie-oscars-2017.jpg-difference-9.rgb │ ├── karlie-oscars-2017.jpg-difference-0.rgb.png │ ├── karlie-oscars-2017.jpg-difference-1.rgb.png │ ├── karlie-oscars-2017.jpg-difference-2.rgb.png │ ├── karlie-oscars-2017.jpg-difference-3.rgb.png │ ├── karlie-oscars-2017.jpg-difference-4.rgb.png │ ├── karlie-oscars-2017.jpg-difference-5.rgb.png │ ├── karlie-oscars-2017.jpg-difference-6.rgb.png │ ├── karlie-oscars-2017.jpg-difference-7.rgb.png │ ├── karlie-oscars-2017.jpg-difference-8.rgb.png │ └── karlie-oscars-2017.jpg-difference-9.rgb.png ├── encode-advanced │ ├── karlie-cfdas-2011.png │ ├── karlie-cfdas-2011.png.rgb │ └── karlie-cfdas-2011.png.rgb.jpg ├── LICENSE ├── encode-basic │ ├── karlie-milan-sp12-2011.rgb │ ├── karlie-milan-sp12-2011.rgb.png │ ├── karlie-milan-sp12-2011-4-2-0-0.0.jpg │ ├── karlie-milan-sp12-2011-4-2-0-0.25.jpg │ ├── karlie-milan-sp12-2011-4-2-0-0.5.jpg │ ├── karlie-milan-sp12-2011-4-2-0-1.0.jpg │ ├── karlie-milan-sp12-2011-4-2-0-2.0.jpg │ ├── karlie-milan-sp12-2011-4-2-0-4.0.jpg │ ├── karlie-milan-sp12-2011-4-2-0-8.0.jpg │ ├── karlie-milan-sp12-2011-4-2-2-0.0.jpg │ ├── karlie-milan-sp12-2011-4-2-2-0.25.jpg │ ├── karlie-milan-sp12-2011-4-2-2-0.5.jpg │ ├── karlie-milan-sp12-2011-4-2-2-1.0.jpg │ ├── karlie-milan-sp12-2011-4-2-2-2.0.jpg │ ├── karlie-milan-sp12-2011-4-2-2-4.0.jpg │ ├── karlie-milan-sp12-2011-4-2-2-8.0.jpg │ ├── karlie-milan-sp12-2011-4-4-0-0.0.jpg │ ├── karlie-milan-sp12-2011-4-4-0-0.25.jpg │ ├── karlie-milan-sp12-2011-4-4-0-0.5.jpg │ ├── karlie-milan-sp12-2011-4-4-0-1.0.jpg │ ├── karlie-milan-sp12-2011-4-4-0-2.0.jpg │ ├── karlie-milan-sp12-2011-4-4-0-4.0.jpg │ ├── karlie-milan-sp12-2011-4-4-0-8.0.jpg │ ├── karlie-milan-sp12-2011-4-4-4-0.0.jpg │ ├── karlie-milan-sp12-2011-4-4-4-0.25.jpg │ ├── karlie-milan-sp12-2011-4-4-4-0.5.jpg │ ├── karlie-milan-sp12-2011-4-4-4-1.0.jpg │ ├── karlie-milan-sp12-2011-4-4-4-2.0.jpg │ ├── karlie-milan-sp12-2011-4-4-4-4.0.jpg │ ├── karlie-milan-sp12-2011-4-4-4-8.0.jpg │ ├── karlie-milan-sp12-2011-4-2-0-0.125.jpg │ ├── karlie-milan-sp12-2011-4-2-2-0.125.jpg │ ├── karlie-milan-sp12-2011-4-4-0-0.125.jpg │ └── karlie-milan-sp12-2011-4-4-4-0.125.jpg └── attribution.md ├── utils ├── tests ├── unit-test ├── integration-test ├── regression-test ├── fuzz-test └── examples ├── .gitignore ├── Sources ├── JPEG │ ├── docs.docc │ │ ├── General.md │ │ ├── JPEG.Layout.md │ │ ├── JPEG.Data.Rectangular.md │ │ ├── JPEG.Data.Planar.md │ │ ├── JPEG.Data.Spectral.md │ │ └── JPEG.md │ ├── debug.swift │ └── common.swift ├── JPEGInspection │ ├── String (ext).swift │ └── Highlight.swift └── JPEGSystem │ └── os.swift ├── NOTICE ├── Snippets ├── DecodeBasic.swift ├── EncodeBasic.swift ├── Recompress.swift ├── InMemory.swift ├── EncodeAdvanced.swift ├── DecodeAdvanced.swift ├── CustomColor.swift ├── Rotate.swift └── DecodeOnline.swift ├── Package.swift ├── README.md └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [@tayloraswift] 2 | -------------------------------------------------------------------------------- /tests/unit/result.swift: -------------------------------------------------------------------------------- 1 | ../common/result.swift -------------------------------------------------------------------------------- /tests/integration/result.swift: -------------------------------------------------------------------------------- 1 | ../common/result.swift -------------------------------------------------------------------------------- /tests/regression/result.swift: -------------------------------------------------------------------------------- 1 | ../common/result.swift -------------------------------------------------------------------------------- /tests/regression/gold/attribution.md: -------------------------------------------------------------------------------- 1 | ../../integration/decode/attribution.md -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Dianna Ma 2 | Dianna Ma 3 | -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-1.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/color-progressive-1.jpg -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-2.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/color-progressive-2.jpg -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-3.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/color-progressive-3.jpg -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-4.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/color-progressive-4.jpg -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-1.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/color-sequential-1.jpg -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-2.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/color-sequential-2.jpg -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-3.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/color-sequential-3.jpg -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-4.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/color-sequential-4.jpg -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-progressive-1.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/grayscale-progressive-1.jpg -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-progressive-2.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/grayscale-progressive-2.jpg -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-sequential-1.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/grayscale-sequential-1.jpg -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-sequential-2.jpg: -------------------------------------------------------------------------------- 1 | ../../integration/decode/grayscale-sequential-2.jpg -------------------------------------------------------------------------------- /Scripts/TestAll: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | swift --version 4 | 5 | utils/tests 6 | utils/examples -c release 7 | -------------------------------------------------------------------------------- /examples/custom-color/output.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/custom-color/output.jpg -------------------------------------------------------------------------------- /examples/recompress/original.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/recompress/original.jpg -------------------------------------------------------------------------------- /examples/in-memory/karlie-2011.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/in-memory/karlie-2011.jpg -------------------------------------------------------------------------------- /examples/custom-color/output.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/custom-color/output.jpg.rgb -------------------------------------------------------------------------------- /examples/in-memory/karlie-2011.jpg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/in-memory/karlie-2011.jpg.jpg -------------------------------------------------------------------------------- /examples/in-memory/karlie-2011.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/in-memory/karlie-2011.jpg.rgb -------------------------------------------------------------------------------- /examples/custom-color/output.jpg.rgb-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/custom-color/output.jpg.rgb-8.png -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg -------------------------------------------------------------------------------- /examples/decode-basic/karlie-kwk-2019.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-basic/karlie-kwk-2019.jpg -------------------------------------------------------------------------------- /examples/in-memory/karlie-2011.jpg.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/in-memory/karlie-2011.jpg.rgb.png -------------------------------------------------------------------------------- /examples/rotate/karlie-kwk-wwdc-2017.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/rotate/karlie-kwk-wwdc-2017.jpg -------------------------------------------------------------------------------- /examples/custom-color/output.jpg.rgb-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/custom-color/output.jpg.rgb-16.png -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg.rgb -------------------------------------------------------------------------------- /examples/rotate/karlie-kwk-wwdc-2017-ii.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/rotate/karlie-kwk-wwdc-2017-ii.jpg -------------------------------------------------------------------------------- /examples/rotate/karlie-kwk-wwdc-2017-iii.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/rotate/karlie-kwk-wwdc-2017-iii.jpg -------------------------------------------------------------------------------- /examples/rotate/karlie-kwk-wwdc-2017-iv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/rotate/karlie-kwk-wwdc-2017-iv.jpg -------------------------------------------------------------------------------- /tests/integration/encode/karlie-kloss-1.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/encode/karlie-kloss-1.rgb -------------------------------------------------------------------------------- /examples/decode-basic/karlie-kwk-2019.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-basic/karlie-kwk-2019.jpg.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg -------------------------------------------------------------------------------- /examples/encode-advanced/karlie-cfdas-2011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-advanced/karlie-cfdas-2011.png -------------------------------------------------------------------------------- /examples/recompress/recompressed-full-cycle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/recompress/recompressed-full-cycle.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-sequential-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-sequential-1.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-sequential-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-sequential-2.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-sequential-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-sequential-3.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-sequential-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-sequential-4.jpg -------------------------------------------------------------------------------- /examples/LICENSE: -------------------------------------------------------------------------------- 1 | These example programs are public domain and can be adapted without restrictions. The rest of the library is available under the MPL-2.0. 2 | -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg.rgb.png -------------------------------------------------------------------------------- /examples/decode-basic/karlie-kwk-2019.jpg.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-basic/karlie-kwk-2019.jpg.rgb.png -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011.rgb -------------------------------------------------------------------------------- /examples/recompress/recompressed-requantized.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/recompress/recompressed-requantized.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-progressive-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-progressive-1.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-progressive-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-progressive-2.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-progressive-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-progressive-3.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-progressive-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-progressive-4.jpg -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-1.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-progressive-1.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-1.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-progressive-1.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-2.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-progressive-2.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-2.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-progressive-2.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-3.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-progressive-3.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-3.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-progressive-3.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-4.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-progressive-4.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/color-progressive-4.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-progressive-4.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-1.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-sequential-1.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-1.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-sequential-1.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-2.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-sequential-2.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-2.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-sequential-2.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-3.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-sequential-3.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-3.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-sequential-3.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-4.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-sequential-4.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/color-sequential-4.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/color-sequential-4.jpg.ycc -------------------------------------------------------------------------------- /examples/custom-color/output.jpg.rgb-difference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/custom-color/output.jpg.rgb-difference.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-0.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-0.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-1.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-1.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-2.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-2.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-3.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-3.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-4.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-4.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-5.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-5.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-6.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-6.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-7.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-7.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-8.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-8.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-9.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-9.rgb -------------------------------------------------------------------------------- /examples/encode-advanced/karlie-cfdas-2011.png.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-advanced/karlie-cfdas-2011.png.rgb -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011.rgb.png -------------------------------------------------------------------------------- /tests/integration/decode/grayscale-progressive-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/grayscale-progressive-1.jpg -------------------------------------------------------------------------------- /tests/integration/decode/grayscale-progressive-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/grayscale-progressive-2.jpg -------------------------------------------------------------------------------- /tests/integration/decode/grayscale-sequential-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/grayscale-sequential-1.jpg -------------------------------------------------------------------------------- /tests/integration/decode/grayscale-sequential-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/grayscale-sequential-2.jpg -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-sequential-1.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/grayscale-sequential-1.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-sequential-1.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/grayscale-sequential-1.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-sequential-2.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/grayscale-sequential-2.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-sequential-2.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/grayscale-sequential-2.jpg.ycc -------------------------------------------------------------------------------- /examples/encode-advanced/karlie-cfdas-2011.png.rgb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-advanced/karlie-cfdas-2011.png.rgb.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-progressive-restart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-progressive-restart.jpg -------------------------------------------------------------------------------- /tests/integration/decode/color-sequential-restart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/color-sequential-restart.jpg -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-progressive-1.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/grayscale-progressive-1.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-progressive-1.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/grayscale-progressive-1.jpg.ycc -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-progressive-2.jpg.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/grayscale-progressive-2.jpg.rgb -------------------------------------------------------------------------------- /tests/regression/gold/grayscale-progressive-2.jpg.ycc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/regression/gold/grayscale-progressive-2.jpg.ycc -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg-0.640x432.gray: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg-0.640x432.gray -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg-1.320x216.gray: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg-1.320x216.gray -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg-2.320x216.gray: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg-2.320x216.gray -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-0.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-0.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-1.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-1.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-2.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-2.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-3.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-3.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-4.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-4.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-5.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-5.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-6.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-6.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-7.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-7.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-8.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-8.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-9.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-9.rgb.png -------------------------------------------------------------------------------- /tests/integration/decode/grayscale-sequential-restart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/grayscale-sequential-restart.jpg -------------------------------------------------------------------------------- /tests/integration/encode/attribution.md: -------------------------------------------------------------------------------- 1 | karlie kloss: 2 | 3 | 1. [Erik Drost](https://commons.wikimedia.org/wiki/File:Karlie_Kloss_%2847594281901%29.jpg) (CC BY 2.0) 4 | -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg-0.640x432.gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg-0.640x432.gray.png -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg-1.320x216.gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg-1.320x216.gray.png -------------------------------------------------------------------------------- /examples/decode-advanced/karlie-2019.jpg-2.320x216.gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-advanced/karlie-2019.jpg-2.320x216.gray.png -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-0-0.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-0-0.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-0-0.25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-0-0.25.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-0-0.5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-0-0.5.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-0-1.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-0-1.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-0-2.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-0-2.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-0-4.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-0-4.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-0-8.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-0-8.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-2-0.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-2-0.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-2-0.25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-2-0.25.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-2-0.5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-2-0.5.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-2-1.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-2-1.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-2-2.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-2-2.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-2-4.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-2-4.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-2-8.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-2-8.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-0-0.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-0-0.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-0-0.25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-0-0.25.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-0-0.5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-0-0.5.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-0-1.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-0-1.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-0-2.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-0-2.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-0-4.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-0-4.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-0-8.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-0-8.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-4-0.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-4-0.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-4-0.25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-4-0.25.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-4-0.5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-4-0.5.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-4-1.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-4-1.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-4-2.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-4-2.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-4-4.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-4-4.0.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-4-8.0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-4-8.0.jpg -------------------------------------------------------------------------------- /tests/integration/decode/grayscale-progressive-restart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/tests/integration/decode/grayscale-progressive-restart.jpg -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-0.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-0.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-1.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-1.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-2.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-2.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-3.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-3.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-4.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-4.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-5.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-5.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-6.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-6.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-7.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-7.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-8.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-8.rgb -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-9.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-9.rgb -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-0-0.125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-0-0.125.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-2-2-0.125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-2-2-0.125.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-0-0.125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-0-0.125.jpg -------------------------------------------------------------------------------- /examples/encode-basic/karlie-milan-sp12-2011-4-4-4-0.125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/encode-basic/karlie-milan-sp12-2011-4-4-4-0.125.jpg -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-0.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-0.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-1.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-1.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-2.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-2.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-3.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-3.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-4.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-4.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-5.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-5.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-6.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-6.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-7.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-7.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-8.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-8.rgb.png -------------------------------------------------------------------------------- /examples/decode-online/karlie-oscars-2017.jpg-difference-9.rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tayloraswift/swift-jpeg/HEAD/examples/decode-online/karlie-oscars-2017.jpg-difference-9.rgb.png -------------------------------------------------------------------------------- /utils/tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | utils/unit-test || exit 1 4 | utils/integration-test -c release || exit 1 5 | utils/regression-test -c release || exit 1 6 | utils/fuzz-test -c release -n 16 || exit 1 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .build/ 4 | .build.ssgc/ 5 | .ssgc/ 6 | 7 | /Packages 8 | /*.xcodeproj 9 | /tests/fuzz/data 10 | /tests/integration/decode/*.png 11 | /tests/integration/decode/*.rgb 12 | /tests/integration/encode/*.jpg 13 | -------------------------------------------------------------------------------- /Sources/JPEG/docs.docc/General.md: -------------------------------------------------------------------------------- 1 | # ``General`` 2 | 3 | ## Topics 4 | 5 | ### Range types 6 | 7 | - ``General.Range2`` 8 | 9 | ### Integer storage 10 | 11 | - ``General.Storage`` 12 | - ``General.Storage2`` 13 | - ``General.MutableStorage`` 14 | -------------------------------------------------------------------------------- /tests/unit/main.swift: -------------------------------------------------------------------------------- 1 | import func Foundation.exit 2 | 3 | var failed = false 4 | for (name, function):(String, Test.Function) in Test.cases 5 | { 6 | guard let _:Void = test(function, name: name) 7 | else 8 | { 9 | failed = true 10 | continue 11 | } 12 | } 13 | 14 | failed ? Foundation.exit(-1) : Foundation.exit(0) 15 | -------------------------------------------------------------------------------- /tests/integration/main.swift: -------------------------------------------------------------------------------- 1 | import func Foundation.exit 2 | 3 | var failed = false 4 | for (name, function):(String, Test.Function) in Test.cases 5 | { 6 | guard let _:Void = test(function, name: name) 7 | else 8 | { 9 | failed = true 10 | continue 11 | } 12 | } 13 | 14 | failed ? Foundation.exit(-1) : Foundation.exit(0) 15 | -------------------------------------------------------------------------------- /tests/regression/main.swift: -------------------------------------------------------------------------------- 1 | import func Foundation.exit 2 | 3 | var failed = false 4 | for (name, function):(String, Test.Function) in Test.cases 5 | { 6 | guard let _:Void = test(function, name: name) 7 | else 8 | { 9 | failed = true 10 | continue 11 | } 12 | } 13 | 14 | failed ? Foundation.exit(-1) : Foundation.exit(0) 15 | -------------------------------------------------------------------------------- /.github/workflows/iOS.yml: -------------------------------------------------------------------------------- 1 | name: iOS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | uses: tayloraswift/swift-device-action/.github/workflows/build.yml@master 12 | with: 13 | xcode-scheme: 'JPEG' 14 | destination: ${{ github.workflow }} 15 | -------------------------------------------------------------------------------- /.github/workflows/tvOS.yml: -------------------------------------------------------------------------------- 1 | name: tvOS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | uses: tayloraswift/swift-device-action/.github/workflows/build.yml@master 12 | with: 13 | xcode-scheme: 'JPEG' 14 | destination: ${{ github.workflow }} 15 | -------------------------------------------------------------------------------- /.github/workflows/visionOS.yml: -------------------------------------------------------------------------------- 1 | name: visionOS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | uses: tayloraswift/swift-device-action/.github/workflows/build.yml@master 12 | with: 13 | xcode-scheme: 'JPEG' 14 | destination: ${{ github.workflow }} 15 | -------------------------------------------------------------------------------- /.github/workflows/watchOS.yml: -------------------------------------------------------------------------------- 1 | name: watchOS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | uses: tayloraswift/swift-device-action/.github/workflows/build.yml@master 12 | with: 13 | xcode-scheme: 'JPEG' 14 | destination: ${{ github.workflow }} 15 | -------------------------------------------------------------------------------- /Sources/JPEGInspection/String (ext).swift: -------------------------------------------------------------------------------- 1 | extension String 2 | { 3 | public 4 | static func pad(_ string:String, left count:Int) -> Self 5 | { 6 | .init(repeating: " ", count: count - string.count) + string 7 | } 8 | public 9 | static func pad(_ string:String, right count:Int) -> Self 10 | { 11 | string + .init(repeating: " ", count: count - string.count) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/JPEG/docs.docc/JPEG.Layout.md: -------------------------------------------------------------------------------- 1 | # ``JPEG.Layout`` 2 | 3 | ## Topics 4 | 5 | ### Creating a layout 6 | 7 | - ``init(format:process:components:scans:)`` 8 | 9 | 10 | ### Image modes 11 | 12 | - ``format`` 13 | - ``process`` 14 | 15 | 16 | ### Component membership 17 | 18 | - ``residents`` 19 | - ``recognized`` 20 | 21 | 22 | ### Image structure 23 | 24 | - ``planes`` 25 | - ``definitions`` 26 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | the `swift-jpeg` package was created by dianna ma (@taylorswift). 2 | 3 | you are welcome to contribute to this project at: 4 | 5 | https://github.com/tayloraswift/swift-jpeg 6 | 7 | we do not maintain any other mirrors of this repository, and such 8 | forks of this repository may not carry the most up-to-date code. 9 | 10 | contributors: 11 | 12 | 1. Dianna Ma (@taylorswift, 2021–24) 13 | 1. Valeriy Van (@valeriyvan, 2021–24) 14 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | android: 11 | runs-on: ubuntu-24.04 12 | name: Android 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | - name: Build for Android 18 | uses: skiptools/swift-android-action@v2 19 | with: 20 | run-tests: false 21 | -------------------------------------------------------------------------------- /utils/unit-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | TOOL_NAME="JPEGUnitTests" 3 | error() 4 | { 5 | echo $1 6 | exit 1 7 | } 8 | 9 | check() 10 | { 11 | message=$1 12 | shift 13 | echo $@ 14 | "$@" || error "$message" 15 | } 16 | 17 | check "error: swift build failed" \ 18 | swift build -c release --product $TOOL_NAME 19 | 20 | binaries=".build/release" 21 | if ! [ -f $binaries/$TOOL_NAME ]; then 22 | error "error: missing $TOOL_NAME tool" 23 | fi 24 | 25 | check "error: test failures" \ 26 | $binaries/$TOOL_NAME 27 | -------------------------------------------------------------------------------- /Snippets/DecodeBasic.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | let path:String = "examples/decode-basic/karlie-kwk-2019.jpg" 5 | 6 | guard let image:JPEG.Data.Rectangular = try .decompress(path: path) 7 | else 8 | { 9 | fatalError("failed to open file '\(path)'") 10 | } 11 | 12 | let rgb:[JPEG.RGB] = image.unpack(as: JPEG.RGB.self) 13 | guard let _:Void = (System.File.Destination.open(path: "\(path).rgb") 14 | { 15 | guard let _:Void = $0.write(rgb.flatMap{ [$0.r, $0.g, $0.b] }) 16 | else 17 | { 18 | fatalError("failed to write to file '\(path).rgb'") 19 | } 20 | }) 21 | else 22 | { 23 | fatalError("failed to open file '\(path).rgb'") 24 | } 25 | -------------------------------------------------------------------------------- /Sources/JPEG/docs.docc/JPEG.Data.Rectangular.md: -------------------------------------------------------------------------------- 1 | # ``JPEG.Data.Rectangular`` 2 | 3 | ## Topics 4 | 5 | ### Creating an image 6 | 7 | - ``init(size:layout:metadata:values:)`` 8 | - ``init(size:layout:metadata:)`` 9 | - ``pack(size:layout:metadata:pixels:)`` 10 | - ``decompress(stream:cosite:)`` 11 | - ``decompress(path:cosite:)`` 12 | 13 | 14 | ### Saving an image 15 | 16 | - ``compress(stream:quanta:)`` 17 | - ``compress(path:quanta:)`` 18 | 19 | 20 | ### Querying an image 21 | 22 | - ``size`` 23 | - ``layout`` 24 | - ``metadata`` 25 | 26 | 27 | ### Changing representations 28 | 29 | - ``unpack(as:)`` 30 | - ``decomposed`` 31 | 32 | 33 | ### Accessing samples 34 | 35 | - ``stride`` 36 | - ``subscript(x:y:_:)`` 37 | - ``offset(forKey:)`` 38 | -------------------------------------------------------------------------------- /Sources/JPEG/docs.docc/JPEG.Data.Planar.md: -------------------------------------------------------------------------------- 1 | # ``JPEG.Data.Planar`` 2 | 3 | ## Topics 4 | 5 | ### Creating an image 6 | 7 | - ``init(size:layout:metadata:initializingWith:)`` 8 | - ``init(size:layout:metadata:)`` 9 | - ``decompress(stream:)`` 10 | - ``decompress(path:)`` 11 | 12 | 13 | ### Saving an image 14 | 15 | - ``compress(stream:quanta:)`` 16 | - ``compress(path:quanta:)`` 17 | 18 | 19 | ### Querying an image 20 | 21 | - ``size`` 22 | - ``layout`` 23 | - ``metadata`` 24 | 25 | 26 | ### Changing representations 27 | 28 | - ``interleaved(cosite:)`` 29 | - ``fdct(quanta:)`` 30 | 31 | 32 | ### Accessing planes 33 | 34 | - ``startIndex`` 35 | - ``endIndex`` 36 | - ``subscript(_:)`` 37 | - ``index(forKey:)`` 38 | - ``read(ci:_:)`` 39 | - ``with(ci:_:)`` 40 | -------------------------------------------------------------------------------- /Sources/JPEG/docs.docc/JPEG.Data.Spectral.md: -------------------------------------------------------------------------------- 1 | # ``JPEG.Data.Spectral`` 2 | 3 | ## Topics 4 | 5 | ### Creating an image 6 | 7 | - ``init(size:layout:metadata:quanta:)`` 8 | - ``decompress(stream:)`` 9 | - ``decompress(path:)`` 10 | 11 | 12 | ### Saving an image 13 | 14 | - ``compress(stream:)`` 15 | - ``compress(path:)`` 16 | 17 | 18 | ### Querying an image 19 | 20 | - ``size`` 21 | - ``blocks`` 22 | - ``layout`` 23 | - ``metadata`` 24 | - ``quanta`` 25 | 26 | 27 | ### Editing an image 28 | 29 | - ``set(width:)`` 30 | - ``set(height:)`` 31 | - ``set(quanta:)`` 32 | 33 | 34 | ### Changing representations 35 | 36 | - ``idct`` 37 | - ``encode`` 38 | 39 | 40 | ### Accessing planes 41 | 42 | - ``startIndex`` 43 | - ``endIndex`` 44 | - ``subscript(_:)`` 45 | - ``index(forKey:)`` 46 | - ``read(ci:_:)`` 47 | - ``with(ci:_:)`` 48 | -------------------------------------------------------------------------------- /tests/integration/decode/attribution.md: -------------------------------------------------------------------------------- 1 | color: 2 | 3 | 1. [Bethany Wong](https://commons.wikimedia.org/wiki/File:Karlie_Kloss_%288470966863%29.jpg) (CC BY 2.0) 4 | 2. [Mdelvall](https://commons.wikimedia.org/wiki/File:Karlie_Kloss_and_Nathalie_Colin_%28cropped%29.jpg) (CC BY-SA 2.0) 5 | 3. [Christopher Macsurak](https://commons.wikimedia.org/wiki/File:Karlie_Kloss_at_Anna_Sui.jpg) (CC BY 2.0) 6 | 4. [Christopher Macsurak](https://commons.wikimedia.org/wiki/File:Diane_von_F%C3%BCrstenberg_Spring-Summer_2014_04.jpg) (CC BY 2.0) 7 | 8 | grayscale: 9 | 10 | 1. [Christopher Macsurak](https://commons.wikimedia.org/wiki/File:Karlie_Kloss_@_Donna_Karan_Show_02.jpg) (CC BY 2.0) 11 | 2. [Christopher Macsurak](https://commons.wikimedia.org/wiki/File:Karlie_Kloss_@_Donna_Karan_Show_07.jpg) (CC BY 2.0) 12 | 13 | restart: 14 | 15 | 1. [John “hugo971”](https://www.flickr.com/photos/30375176@N08/6217835941) (CC BY-NC-SA 2.0) 16 | 2. [John “hugo971”](https://www.flickr.com/photos/30375176@N08/5813100679) (CC BY-NC-SA 2.0) 17 | -------------------------------------------------------------------------------- /examples/attribution.md: -------------------------------------------------------------------------------- 1 | `decode-basic`: 2 | 3 | 1. [Shantell Martin](https://www.flickr.com/photos/45424821@N00/49215958632) (CC BY-NC-SA 2.0) 4 | 5 | `encode-basic`: 6 | 7 | 1. [John “hugo971”](https://www.flickr.com/photos/30375176@N08/6198907669) (CC BY-NC-ND 2.0) 8 | 9 | `decode-advanced`: 10 | 11 | 1. [Erik Drost](https://commons.wikimedia.org/wiki/File:Karlie_Kloss_%2847541292642%29.jpg) (CC BY 2.0) 12 | 13 | `encode-advanced`: 14 | 15 | 1. [John “hugo971”](https://www.flickr.com/photos/30375176@N08/5813054049) (CC BY-NC-SA 2.0) 16 | 17 | `in-memory`: 18 | 19 | 1. [John “hugo971”](https://www.flickr.com/photos/30375176@N08/5734204790) (CC BY-NC-ND 2.0) 20 | 21 | `decode-online`: 22 | 23 | 1. [Walt Disney Television](https://www.flickr.com/photos/91795856@N02/33011712301) (CC BY-ND 2.0) 24 | 25 | `recompress`: 26 | 27 | 1. [Karlie’s twitter account](https://twitter.com/karliekloss/status/1258889670357856257) (unknown license) 28 | 29 | `rotate`: 30 | 31 | 1. [Kode With Klossy (Facebook)](https://www.facebook.com/kodewithklossy/photos/kodewithklossy-scholars-attending-a-panel-with-editor-in-chief-at-teen-vogue-ela/1756483347711968/) (unknown) 32 | -------------------------------------------------------------------------------- /.github/workflows/Tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-24.04 12 | name: Ubuntu 24.04 13 | steps: 14 | - name: Install Swift 15 | uses: tayloraswift/swift-install-action@master 16 | with: 17 | swift-prefix: "swift-6.0.3-release/ubuntu2404/swift-6.0.3-RELEASE" 18 | swift-id: "swift-6.0.3-RELEASE-ubuntu24.04" 19 | 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Install imagemagick 24 | run: sudo apt install -y imagemagick 25 | 26 | - name: Run tests 27 | run: Scripts/TestAll 28 | 29 | macos: 30 | runs-on: macos-15 31 | name: macOS 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v3 35 | 36 | - name: Install imagemagick 37 | run: brew install imagemagick 38 | 39 | - name: Run tests 40 | run: Scripts/TestAll 41 | -------------------------------------------------------------------------------- /utils/integration-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEFAULT_BUILD_CONFIGURATION="debug" 3 | TOOL_NAME="JPEGIntegrationTests" 4 | 5 | usage() 6 | { 7 | echo "usage: utils/$TOOL_NAME [OPTION...] 8 | -c, --configuration swift build configuration mode, default '$DEFAULT_BUILD_CONFIGURATION'" 9 | } 10 | 11 | error() 12 | { 13 | echo $1 14 | exit 1 15 | } 16 | 17 | check() 18 | { 19 | message=$1 20 | shift 21 | echo $@ 22 | "$@" || error "$message" 23 | } 24 | 25 | build_configuration=$DEFAULT_BUILD_CONFIGURATION 26 | 27 | while [ "$1" != "" ] ; do 28 | case $1 in 29 | -c | --configuration ) 30 | shift 31 | build_configuration=$1 32 | ;; 33 | * ) 34 | usage 35 | exit 1 36 | esac 37 | shift 38 | done 39 | 40 | check "error: swift build failed" \ 41 | swift build -c $build_configuration --product $TOOL_NAME 42 | 43 | binaries=".build/$build_configuration" 44 | if ! [ -f $binaries/$TOOL_NAME ]; then 45 | error "error: missing $TOOL_NAME tool" 46 | fi 47 | 48 | check "error: test failures" \ 49 | $binaries/$TOOL_NAME 50 | 51 | data=(tests/integration/decode/*.jpg) 52 | for file in ${data[@]} ; do 53 | size=$(identify -format "%wx%h" jpg:$file) 54 | convert -depth 8 -size $size "rgb:$file.rgb" "png:$file.rgb.png" 55 | convert -depth 8 -size $size -define jpeg:dct-method=float "jpeg:$file" rgb:- |\ 56 | convert -depth 8 -size $size rgb:- "png:$file.png" 57 | done 58 | -------------------------------------------------------------------------------- /.github/workflows/Documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-24.04 12 | name: Ubuntu 24.04 13 | 14 | steps: 15 | - name: Install Swift 16 | uses: tayloraswift/swift-install-action@master 17 | with: 18 | swift-prefix: "swift-6.0.3-release/ubuntu2404/swift-6.0.3-RELEASE" 19 | swift-id: "swift-6.0.3-RELEASE-ubuntu24.04" 20 | 21 | - name: Install Unidoc 22 | uses: tayloraswift/swift-unidoc-action@master 23 | 24 | - name: Checkout repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Validate documentation 28 | run: | 29 | unidoc compile \ 30 | --swift-toolchain $SWIFT_INSTALLATION \ 31 | --ci fail-on-errors \ 32 | --project-path . 33 | 34 | macos: 35 | runs-on: macos-15 36 | name: macOS 37 | steps: 38 | - name: Install Unidoc 39 | uses: tayloraswift/swift-unidoc-action@master 40 | 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | - name: Validate documentation 45 | run: | 46 | unidoc compile \ 47 | --ci fail-on-errors \ 48 | --project-path . 49 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "swift-jpeg", 6 | products: 7 | [ 8 | .library(name: "JPEG", targets: ["JPEG"]), 9 | .library(name: "JPEGSystem", targets: ["JPEGSystem"]), 10 | ], 11 | targets: 12 | [ 13 | .target(name: "JPEG"), 14 | 15 | .target(name: "JPEGSystem", 16 | dependencies: ["JPEG"]), 17 | 18 | .target(name: "JPEGInspection"), 19 | 20 | .executableTarget(name: "JPEGFuzzer", 21 | dependencies: ["JPEG", "JPEGInspection", "JPEGSystem"], 22 | path: "tests/fuzz", 23 | exclude: [ 24 | "data/", 25 | ] 26 | ), 27 | 28 | .executableTarget(name: "JPEGComparator", 29 | dependencies: ["JPEG", "JPEGInspection", "JPEGSystem"], 30 | path: "tests/compare"), 31 | 32 | .executableTarget(name: "JPEGUnitTests", 33 | dependencies: ["JPEG", "JPEGInspection", "JPEGSystem"], 34 | path: "tests/unit"), 35 | 36 | .executableTarget(name: "JPEGRegressionTests", 37 | dependencies: ["JPEG", "JPEGInspection", "JPEGSystem"], 38 | path: "tests/regression", 39 | exclude: [ 40 | "gold/", 41 | ]), 42 | 43 | .executableTarget(name: "JPEGIntegrationTests", 44 | dependencies: ["JPEG", "JPEGInspection", "JPEGSystem"], 45 | path: "tests/integration", 46 | exclude: [ 47 | "decode/", 48 | "encode/", 49 | ]), 50 | ], 51 | swiftLanguageVersions: [.v4_2, .v5] 52 | ) 53 | -------------------------------------------------------------------------------- /utils/regression-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEFAULT_BUILD_CONFIGURATION="debug" 3 | TOOL_NAME="JPEGRegressionTests" 4 | 5 | usage() 6 | { 7 | echo "usage: utils/$TOOL_NAME [OPTION...] 8 | -c, --configuration swift build configuration mode, default '$DEFAULT_BUILD_CONFIGURATION' 9 | -u, --update remove and regenerate golden outputs (tests will fail)" 10 | } 11 | 12 | error() 13 | { 14 | echo $1 15 | exit 1 16 | } 17 | 18 | check() 19 | { 20 | message=$1 21 | shift 22 | echo $@ 23 | "$@" || error "$message" 24 | } 25 | 26 | build_configuration=$DEFAULT_BUILD_CONFIGURATION 27 | 28 | while [ "$1" != "" ] ; do 29 | case $1 in 30 | -c | --configuration ) 31 | shift 32 | build_configuration=$1 33 | ;; 34 | -u | --update ) 35 | shift 36 | gold=(tests/regression/gold/*.jpg.ycc) 37 | echo "the following ${#gold[@]} golden outputs will be removed and regenerated:" 38 | for file in ${gold[@]} ; do 39 | echo " $file" 40 | done 41 | echo "note: regression tests will fail. regression tests will pass on the next run." 42 | echo -n "press enter to confirm>" 43 | read confirmation 44 | rm ${gold[@]} 45 | ;; 46 | * ) 47 | usage 48 | exit 1 49 | esac 50 | shift 51 | done 52 | 53 | check "error: swift build failed" \ 54 | swift build -c $build_configuration --product $TOOL_NAME 55 | 56 | binaries=".build/$build_configuration" 57 | if ! [ -f $binaries/$TOOL_NAME ]; then 58 | error "error: missing $TOOL_NAME tool" 59 | fi 60 | 61 | check "error: test failures" \ 62 | $binaries/$TOOL_NAME 63 | -------------------------------------------------------------------------------- /Sources/JPEG/docs.docc/JPEG.md: -------------------------------------------------------------------------------- 1 | # ``/JPEG`` 2 | 3 | Decode, inspect, edit, and encode JPEG images. 4 | 5 | 6 | ## Topics 7 | 8 | ### Color spaces 9 | 10 | - ``JPEG.Format`` 11 | - ``JPEG.Color`` 12 | - ``JPEG.YCbCr`` 13 | - ``JPEG.RGB`` 14 | - ``JPEG.Common`` 15 | 16 | 17 | ### Image metadata 18 | 19 | - ``JPEG.Metadata`` 20 | - ``JPEG.JFIF`` 21 | - ``JPEG.EXIF`` 22 | 23 | 24 | ### Image headers 25 | 26 | - ``JPEG.Header`` 27 | - ``JPEG.Header.HeightRedefinition`` 28 | - ``JPEG.Header.RestartInterval`` 29 | - ``JPEG.Header.Frame`` 30 | - ``JPEG.Header.Scan`` 31 | 32 | 33 | ### Image tables 34 | 35 | - ``JPEG.AnyTable`` 36 | - ``JPEG.Table`` 37 | - ``JPEG.Table.Huffman`` 38 | - ``JPEG.Table.Quantization`` 39 | 40 | 41 | ### Entropy coding 42 | 43 | - ``JPEG.Bitstream.AnySymbol`` 44 | - ``JPEG.Bitstream.Symbol`` 45 | - ``JPEG.Bitstream.Symbol.DC`` 46 | - ``JPEG.Bitstream.Symbol.AC`` 47 | - ``JPEG.Bitstream`` 48 | 49 | 50 | ### Data IO and file structure 51 | 52 | - ``JPEG.Bytestream`` 53 | - ``JPEG.Bytestream.Source`` 54 | - ``JPEG.Bytestream.Destination`` 55 | - ``JPEG.Process`` 56 | - ``JPEG.Marker`` 57 | 58 | 59 | ### Image representations 60 | 61 | - ``JPEG.Data`` 62 | - ``JPEG.Data.Spectral`` 63 | - ``JPEG.Data.Planar`` 64 | - ``JPEG.Data.Rectangular`` 65 | 66 | 67 | ### Image layout and decomposition 68 | 69 | - ``JPEG.Component`` 70 | - ``JPEG.Component.Key`` 71 | - ``JPEG.Scan`` 72 | - ``JPEG.Scan.Component`` 73 | - ``JPEG.Layout`` 74 | 75 | 76 | ### Image quality 77 | 78 | - ``JPEG.Table.Quantization.Key`` 79 | - ``JPEG.CompressionLevel`` 80 | 81 | 82 | ### Manual decoding 83 | 84 | - ``JPEG.Context`` 85 | 86 | 87 | ### Error handling 88 | 89 | - ``JPEG.Error`` 90 | - ``JPEG.LexingError`` 91 | - ``JPEG.ParsingError`` 92 | - ``JPEG.DecodingError`` 93 | - ``JPEG.FormattingError`` 94 | - ``JPEG.SerializingError`` 95 | - ``JPEG.EncodingError`` 96 | -------------------------------------------------------------------------------- /Snippets/EncodeBasic.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | let path:String = "examples/encode-basic/karlie-milan-sp12-2011", 5 | size:(x:Int, y:Int) = (400, 665) 6 | guard let rgb:[JPEG.RGB] = (System.File.Source.open(path: "\(path).rgb") 7 | { 8 | guard let data:[UInt8] = $0.read(count: 3 * size.x * size.y) 9 | else 10 | { 11 | fatalError("failed to read from file '\(path).rgb'") 12 | } 13 | 14 | return (0 ..< size.x * size.y).map 15 | { 16 | (i:Int) -> JPEG.RGB in 17 | .init(data[3 * i], data[3 * i + 1], data[3 * i + 2]) 18 | } 19 | }) 20 | else 21 | { 22 | fatalError("failed to open file '\(path).rgb'") 23 | } 24 | 25 | for factor:(luminance:(Int, Int), chrominance:(Int, Int), name:String) in 26 | [ 27 | ((1, 1), (1, 1), "4-4-4"), 28 | ((1, 2), (1, 1), "4-4-0"), 29 | ((2, 1), (1, 1), "4-2-2"), 30 | ((2, 2), (1, 1), "4-2-0"), 31 | ] 32 | { 33 | let layout:JPEG.Layout = .init( 34 | format: .ycc8, 35 | process: .baseline, 36 | components: 37 | [ 38 | 1: (factor: factor.luminance, qi: 0 as JPEG.Table.Quantization.Key), 39 | 2: (factor: factor.chrominance, qi: 1 as JPEG.Table.Quantization.Key), 40 | 3: (factor: factor.chrominance, qi: 1 as JPEG.Table.Quantization.Key), 41 | ], 42 | scans: 43 | [ 44 | .sequential((1, \.0, \.0)), 45 | .sequential((2, \.1, \.1), (3, \.1, \.1)) 46 | ]) 47 | let jfif:JPEG.JFIF = .init(version: .v1_2, density: (1, 1, .centimeters)) 48 | let image:JPEG.Data.Rectangular = 49 | .pack(size: size, layout: layout, metadata: [.jfif(jfif)], pixels: rgb) 50 | 51 | for level:Double in [0.0, 0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0] 52 | { 53 | try image.compress(path: "\(path)-\(factor.name)-\(level).jpg", quanta: 54 | [ 55 | 0: JPEG.CompressionLevel.luminance( level).quanta, 56 | 1: JPEG.CompressionLevel.chrominance(level).quanta 57 | ]) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Snippets/Recompress.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | let path:String = "examples/recompress/original.jpg" 5 | guard let original:JPEG.Data.Spectral = try .decompress(path: path) 6 | else 7 | { 8 | fatalError("failed to open file '\(path)'") 9 | } 10 | 11 | let format:JPEG.Common = .ycc8 12 | let Y:JPEG.Component.Key = format.components[0], 13 | Cb:JPEG.Component.Key = format.components[1], 14 | Cr:JPEG.Component.Key = format.components[2] 15 | 16 | let layout:JPEG.Layout = .init( 17 | format: format, 18 | process: .progressive(coding: .huffman, differential: false), 19 | components: original.layout.components, 20 | scans: 21 | [ 22 | .progressive((Y, \.0), (Cb, \.1), (Cr, \.1), bits: 0...), 23 | 24 | .progressive((Y, \.0), band: 1 ..< 64, bits: 0...), 25 | .progressive((Cb, \.0), band: 1 ..< 64, bits: 0...), 26 | .progressive((Cr, \.0), band: 1 ..< 64, bits: 0...) 27 | ]) 28 | 29 | var recompressed:JPEG.Data.Spectral = .init( 30 | size: original.size, 31 | layout: layout, 32 | metadata: 33 | [ 34 | .jfif(.init(version: .v1_2, density: (1, 1, .centimeters))), 35 | ], 36 | quanta: original.quanta.mapValues 37 | { 38 | [$0[0]] + $0.dropFirst().map{ min($0 * 3 as UInt16, 255) } 39 | }) 40 | 41 | for ci:JPEG.Component.Key in recompressed.layout.recognized 42 | { 43 | original.read(ci: ci) 44 | { 45 | (plane:JPEG.Data.Spectral.Plane, quanta:JPEG.Table.Quantization) in 46 | 47 | recompressed.with(ci: ci) 48 | { 49 | for b:((x:Int, y:Int), (x:Int, y:Int)) in zip(plane.indices, $0.indices) 50 | { 51 | for z:Int in 0 ..< 64 52 | { 53 | let coefficient:Int16 = .init(quanta[z: z]) * plane[x: b.0.x, y: b.0.y, z: z] 54 | let rescaled:Double = .init(coefficient) / .init($1[z: z]) 55 | $0[x: b.1.x, y: b.1.y, z: z] = .init(rescaled + (0.3 * (rescaled < 0 ? -1 : 1))) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | try recompressed.compress(path: "examples/recompress/recompressed-requantized.jpg") 63 | -------------------------------------------------------------------------------- /Snippets/InMemory.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | extension System 5 | { 6 | struct Blob 7 | { 8 | private(set) 9 | var data:[UInt8], 10 | position:Int 11 | } 12 | } 13 | extension System.Blob:JPEG.Bytestream.Source, JPEG.Bytestream.Destination 14 | { 15 | init(_ data:[UInt8]) 16 | { 17 | self.data = data 18 | self.position = data.startIndex 19 | } 20 | 21 | mutating 22 | func read(count:Int) -> [UInt8]? 23 | { 24 | guard self.position + count <= data.endIndex 25 | else 26 | { 27 | return nil 28 | } 29 | 30 | defer 31 | { 32 | self.position += count 33 | } 34 | 35 | return .init(self.data[self.position ..< self.position + count]) 36 | } 37 | 38 | mutating 39 | func write(_ bytes:[UInt8]) -> Void? 40 | { 41 | self.data.append(contentsOf: bytes) 42 | return () 43 | } 44 | } 45 | 46 | let path:String = "examples/in-memory/karlie-2011.jpg" 47 | guard let data:[UInt8] = (System.File.Source.open(path: path) 48 | { 49 | (source:inout System.File.Source) -> [UInt8]? in 50 | 51 | guard let count:Int = source.count 52 | else 53 | { 54 | return nil 55 | } 56 | return source.read(count: count) 57 | } ?? nil) 58 | else 59 | { 60 | fatalError("failed to open or read file '\(path)'") 61 | } 62 | 63 | var blob:System.Blob = .init(data) 64 | // read from blob 65 | let spectral:JPEG.Data.Spectral = try .decompress(stream: &blob) 66 | let image:JPEG.Data.Rectangular = spectral.idct().interleaved() 67 | let rgb:[JPEG.RGB] = image.unpack(as: JPEG.RGB.self) 68 | guard let _:Void = (System.File.Destination.open(path: "\(path).rgb") 69 | { 70 | guard let _:Void = $0.write(rgb.flatMap{ [$0.r, $0.g, $0.b] }) 71 | else 72 | { 73 | fatalError("failed to write to file '\(path).rgb'") 74 | } 75 | }) 76 | else 77 | { 78 | fatalError("failed to open file '\(path).rgb'") 79 | } 80 | 81 | // write to blob 82 | blob = .init([]) 83 | try spectral.compress(stream: &blob) 84 | guard let _:Void = (System.File.Destination.open(path: "\(path).jpg") 85 | { 86 | guard let _:Void = $0.write(blob.data) 87 | else 88 | { 89 | fatalError("failed to write to file '\(path).jpg'") 90 | } 91 | }) 92 | else 93 | { 94 | fatalError("failed to open file '\(path).jpg'") 95 | } 96 | -------------------------------------------------------------------------------- /utils/fuzz-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEFAULT_BUILD_CONFIGURATION="debug" 3 | DEFAULT_COUNT="16" 4 | DEFAULT_PREFIX="tests/fuzz/data" 5 | 6 | usage() 7 | { 8 | echo "usage: utils/fuzz-test [OPTION...] 9 | -c, --configuration swift build configuration mode, default '$DEFAULT_BUILD_CONFIGURATION' 10 | -n, --count number of 8x8 test jpegs to generate, default $DEFAULT_COUNT 11 | -p, --prefix directory to write test images to, default '$DEFAULT_PREFIX'" 12 | } 13 | 14 | error() 15 | { 16 | echo $1 17 | exit 1 18 | } 19 | 20 | check() 21 | { 22 | message=$1 23 | shift 24 | echo $@ 25 | "$@" || error "$message" 26 | } 27 | 28 | until [ $PWD == "/" ] || [ $PWD == $HOME ] || [ -f "Package.swift" ]; do 29 | cd .. || break 30 | done 31 | 32 | if ! [ -f "Package.swift" ]; then 33 | error "error: not an spm repository" 34 | fi 35 | 36 | prefix=$DEFAULT_PREFIX 37 | count=$DEFAULT_COUNT 38 | build_configuration=$DEFAULT_BUILD_CONFIGURATION 39 | 40 | JPEG_DIRECTORY_NAME="jpeg" 41 | YCC_DIRECTORY_NAME="ycc" 42 | JPEG_EXTENSION="jpg" 43 | YCC_EXTENSION="ycc" 44 | 45 | while [ "$1" != "" ] ; do 46 | case $1 in 47 | -c | --configuration ) 48 | shift 49 | build_configuration=$1 50 | ;; 51 | -n | --count ) 52 | shift 53 | count=$1 54 | ;; 55 | -p | --prefix ) 56 | shift 57 | prefix=$1 58 | ;; 59 | * ) 60 | usage 61 | exit 1 62 | esac 63 | shift 64 | done 65 | 66 | jpeg_pattern=$prefix/*.$JPEG_EXTENSION 67 | ycc_pattern=$prefix/*.$YCC_EXTENSION 68 | 69 | check "error: swift build failed" \ 70 | swift build -c $build_configuration --product JPEGFuzzer 71 | check "error: swift build failed" \ 72 | swift build -c $build_configuration --product JPEGComparator 73 | 74 | binaries=".build/$build_configuration" 75 | if ! [ -f $binaries/JPEGFuzzer ]; then 76 | error "error: missing fuzzer tool" 77 | fi 78 | 79 | mkdir $prefix &> /dev/null 80 | rm $jpeg_pattern &> /dev/null 81 | 82 | check "error: test image generation failed" \ 83 | $binaries/JPEGFuzzer --count $count --path $prefix 84 | 85 | rm $ycc_pattern &> /dev/null 86 | 87 | for file in $jpeg_pattern ; do 88 | check "error: system jpeg-to-data conversion failed" \ 89 | convert -depth 8 -define jpeg:dct-method=float "jpeg:$file" "ycbcr:$file.$YCC_EXTENSION" 90 | done 91 | 92 | $binaries/JPEGComparator $jpeg_pattern 93 | -------------------------------------------------------------------------------- /Snippets/EncodeAdvanced.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | let path:String = "examples/encode-advanced/karlie-cfdas-2011.png.rgb", 5 | size:(x:Int, y:Int) = (600, 900) 6 | guard let rgb:[JPEG.RGB] = (System.File.Source.open(path: path) 7 | { 8 | guard let data:[UInt8] = $0.read(count: 3 * size.x * size.y) 9 | else 10 | { 11 | fatalError("failed to read from file '\(path)'") 12 | } 13 | 14 | return (0 ..< size.x * size.y).map 15 | { 16 | (i:Int) -> JPEG.RGB in 17 | .init(data[3 * i], data[3 * i + 1], data[3 * i + 2]) 18 | } 19 | }) 20 | else 21 | { 22 | fatalError("failed to open file '\(path)'") 23 | } 24 | 25 | 26 | let format:JPEG.Common = .ycc8 27 | let Y:JPEG.Component.Key = format.components[0], 28 | Cb:JPEG.Component.Key = format.components[1], 29 | Cr:JPEG.Component.Key = format.components[2] 30 | 31 | let layout:JPEG.Layout = .init( 32 | format: format, 33 | process: .progressive(coding: .huffman, differential: false), 34 | components: 35 | [ 36 | Y: (factor: (2, 1), qi: 0), // 4:2:2 subsampling 37 | Cb: (factor: (1, 1), qi: 1), 38 | Cr: (factor: (1, 1), qi: 1), 39 | ], 40 | scans: 41 | [ 42 | .progressive((Y, \.0), (Cb, \.1), (Cr, \.1), bits: 2...), 43 | .progressive( Y, Cb, Cr , bit: 1 ), 44 | .progressive( Y, Cb, Cr , bit: 0 ), 45 | 46 | .progressive((Y, \.0), band: 1 ..< 64, bits: 1...), 47 | 48 | .progressive((Cb, \.0), band: 1 ..< 6, bits: 1...), 49 | .progressive((Cr, \.0), band: 1 ..< 6, bits: 1...), 50 | 51 | .progressive((Cb, \.0), band: 6 ..< 64, bits: 1...), 52 | .progressive((Cr, \.0), band: 6 ..< 64, bits: 1...), 53 | 54 | .progressive((Y, \.0), band: 1 ..< 64, bit: 0 ), 55 | .progressive((Cb, \.0), band: 1 ..< 64, bit: 0 ), 56 | .progressive((Cr, \.0), band: 1 ..< 64, bit: 0 ), 57 | ]) 58 | 59 | for (tables, scans):([JPEG.Table.Quantization.Key], [JPEG.Scan]) in layout.definitions 60 | { 61 | print(""" 62 | define quantization tables: 63 | [ 64 | \(tables.map(String.init(describing:)).joined(separator: "\n ")) 65 | ] 66 | """) 67 | print(""" 68 | scans: \(scans.count) scans 69 | """) 70 | } 71 | 72 | for (c, (component, qi)):(Int, (component:JPEG.Component, qi:JPEG.Table.Quantization.Key)) in layout.planes.enumerated() 73 | { 74 | print(""" 75 | plane \(c) 76 | { 77 | sampling factor : (\(component.factor.x), \(component.factor.y)) 78 | quantization table : \(qi) 79 | quantization selector : \\.\(String.init(selector: component.selector)) 80 | } 81 | """) 82 | } 83 | 84 | let comment:[UInt8] = .init("the way u say ‘important’ is important".utf8) 85 | let rectangular:JPEG.Data.Rectangular = 86 | .pack(size: size, layout: layout, metadata: [.comment(data: comment)], pixels: rgb) 87 | 88 | let planar:JPEG.Data.Planar = rectangular.decomposed() 89 | let spectral:JPEG.Data.Spectral = planar.fdct(quanta: 90 | [ 91 | 0: [1, 2, 2, 3, 3, 3] + .init(repeating: 4, count: 58), 92 | 1: [1, 2, 2, 5, 5, 5] + .init(repeating: 30, count: 58), 93 | ]) 94 | 95 | guard let _:Void = try spectral.compress(path: "\(path).jpg") 96 | else 97 | { 98 | fatalError("failed to open file '\(path).jpg'") 99 | } 100 | -------------------------------------------------------------------------------- /tests/common/result.swift: -------------------------------------------------------------------------------- 1 | import JPEGInspection 2 | 3 | enum Test 4 | { 5 | struct Failure:Swift.Error 6 | { 7 | let message:String 8 | } 9 | 10 | enum Function 11 | { 12 | case void( () -> Result) 13 | case string_int2((String, (x:Int, y:Int)) -> Result, [(String, (x:Int, y:Int))]) 14 | case string( (String) -> Result, [String]) 15 | case int( (Int) -> Result, [Int]) 16 | } 17 | } 18 | 19 | func test(_ function:Test.Function, name:String) -> Void? 20 | { 21 | var successes:Int = 0 22 | var failures:[(name:String?, message:String)] = [] 23 | switch function 24 | { 25 | case .void(let function): 26 | switch function() 27 | { 28 | case .success: 29 | successes += 1 30 | case .failure(let failure): 31 | failures.append((nil, failure.message)) 32 | } 33 | case .string_int2(let function, let cases): 34 | for arguments:(String, (x:Int, y:Int)) in cases 35 | { 36 | switch function(arguments.0, arguments.1) 37 | { 38 | case .success: 39 | successes += 1 40 | case .failure(let failure): 41 | failures.append(("('\(arguments.0)', \(arguments.1))", failure.message)) 42 | } 43 | } 44 | case .string(let function, let cases): 45 | for argument:String in cases 46 | { 47 | switch function(argument) 48 | { 49 | case .success: 50 | successes += 1 51 | case .failure(let failure): 52 | failures.append((argument, failure.message)) 53 | } 54 | } 55 | case .int(let function, let cases): 56 | for argument:Int in cases 57 | { 58 | switch function(argument) 59 | { 60 | case .success: 61 | successes += 1 62 | case .failure(let failure): 63 | failures.append(("n = \(argument)", failure.message)) 64 | } 65 | } 66 | } 67 | 68 | var width:Int 69 | { 70 | 80 71 | } 72 | var white:(Double, Double, Double) 73 | { 74 | (1, 1, 1) 75 | } 76 | var red:(Double, Double, Double) 77 | { 78 | (1, 0.4, 0.3) 79 | } 80 | switch (successes, failures.count) 81 | { 82 | case (1, 0): 83 | Highlight.print(.pad(" test '\(name)' passed ", right: width), highlight: white) 84 | case (let succeeded, 0): 85 | Highlight.print(.pad(" test '\(name)' passed (\(succeeded) cases)", right: width), highlight: white) 86 | case (0, 1): 87 | Highlight.print(.pad(" test '\(name)' failed ", right: width), highlight: red) 88 | case (let succeeded, let failed): 89 | Highlight.print(.pad(" test '\(name)' failed (\(succeeded + failed) cases, \(failed) failed)", right: width), highlight: red) 90 | } 91 | for (i, failure):(Int, (name:String?, message:String)) in failures.enumerated() 92 | { 93 | if let name:String = failure.name 94 | { 95 | Highlight.print(" [\(String.pad("\(i)", left: 2))] case '\(name)' failed: \(failure.message)", color: red) 96 | } 97 | else 98 | { 99 | Highlight.print(" [\(String.pad("\(i)", left: 2))]: \(failure.message)", color: red) 100 | } 101 | } 102 | 103 | return failures.count > 0 ? nil : () 104 | } 105 | -------------------------------------------------------------------------------- /utils/examples: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DEFAULT_BUILD_CONFIGURATION="debug" 3 | SCRIPT_NAME="examples" 4 | 5 | usage() 6 | { 7 | echo "usage: utils/$SCRIPT_NAME [OPTION...] 8 | -c, --configuration swift build configuration mode, default '$DEFAULT_BUILD_CONFIGURATION'" 9 | } 10 | 11 | error() 12 | { 13 | echo $1 14 | exit 1 15 | } 16 | 17 | check() 18 | { 19 | message=$1 20 | shift 21 | echo $@ 22 | "$@" || error "$message" 23 | } 24 | 25 | build_configuration=$DEFAULT_BUILD_CONFIGURATION 26 | 27 | while [ "$1" != "" ] ; do 28 | case $1 in 29 | -c | --configuration ) 30 | shift 31 | build_configuration=$1 32 | ;; 33 | * ) 34 | usage 35 | exit 1 36 | esac 37 | shift 38 | done 39 | 40 | swift build -c $build_configuration 41 | 42 | # run `DecodeBasic` example 43 | check "error: runtime error" \ 44 | .build/$build_configuration/DecodeBasic 45 | 46 | for file in examples/decode-basic/*.jpg ; do 47 | size=$(identify -format "%wx%h" jpg:$file) 48 | convert -depth 8 -size $size "rgb:$file.rgb" "png:$file.rgb.png" 49 | done 50 | 51 | # run `EncodeBasic` example 52 | check "error: runtime error" \ 53 | .build/$build_configuration/EncodeBasic 54 | 55 | # run `DecodeAdvanced` example 56 | check "error: runtime error" \ 57 | .build/$build_configuration/DecodeAdvanced 58 | 59 | for file in examples/decode-advanced/*.gray ; do 60 | size=$(echo $file | sed -r 's/.*\.([0-9]+)x([0-9]+)\.gray/\1x\2/') 61 | convert -depth 8 -size $size "gray:$file" "png:$file.png" 62 | done 63 | for file in examples/decode-advanced/*.jpg ; do 64 | size=$(identify -format "%wx%h" jpg:$file) 65 | convert -depth 8 -size $size "rgb:$file.rgb" "png:$file.rgb.png" 66 | done 67 | 68 | # run `EncodeAdvanced` example 69 | check "error: runtime error" \ 70 | .build/$build_configuration/EncodeAdvanced 71 | 72 | # run `InMemory` example 73 | check "error: runtime error" \ 74 | .build/$build_configuration/InMemory 75 | for file in examples/in-memory/*.jpg ; do 76 | if [ -f $file.rgb ]; then 77 | size=$(identify -format "%wx%h" jpg:$file) 78 | convert -depth 8 -size $size "rgb:$file.rgb" "png:$file.rgb.png" 79 | fi 80 | done 81 | 82 | # run `DecodeOnline` example 83 | check "error: runtime error" \ 84 | .build/$build_configuration/DecodeOnline 85 | for file in examples/decode-online/*.jpg ; do 86 | size=$(identify -format "%wx%h" jpg:$file) 87 | done 88 | for file in examples/decode-online/*.rgb ; do 89 | convert -depth 8 -size $size "rgb:$file" "png:$file.png" 90 | done 91 | 92 | # run `Recompress` example 93 | check "error: runtime error" \ 94 | .build/$build_configuration/Recompress 95 | 96 | # run `Rotate` example 97 | check "error: runtime error" \ 98 | .build/$build_configuration/Rotate examples/rotate/karlie-kwk-wwdc-2017.jpg examples/rotate/karlie-kwk-wwdc-2017-ii.jpg --rotation ii 99 | check "error: runtime error" \ 100 | .build/$build_configuration/Rotate examples/rotate/karlie-kwk-wwdc-2017.jpg examples/rotate/karlie-kwk-wwdc-2017-iii.jpg --rotation iii 101 | check "error: runtime error" \ 102 | .build/$build_configuration/Rotate examples/rotate/karlie-kwk-wwdc-2017.jpg examples/rotate/karlie-kwk-wwdc-2017-iv.jpg --rotation iv 103 | 104 | # run `CustomColor` example 105 | check "error: runtime error" \ 106 | .build/$build_configuration/CustomColor 107 | for file in examples/custom-color/*.rgb ; do 108 | convert -depth 16 -endian msb -size 1000x200 rgb:$file -depth 16 png:$file-16.png 109 | convert -depth 16 -endian msb -size 1000x200 rgb:$file -depth 8 png:$file-8.png 110 | done 111 | -------------------------------------------------------------------------------- /Sources/JPEGInspection/Highlight.swift: -------------------------------------------------------------------------------- 1 | public 2 | enum Highlight 3 | { 4 | public 5 | static let bold:String = "\u{1B}[1m" 6 | public 7 | static let reset:String = "\u{1B}[0m" 8 | 9 | public 10 | static func fg(_ color:(r:UInt8, g:UInt8, b:UInt8)?) -> String 11 | { 12 | if let color:(r:UInt8, g:UInt8, b:UInt8) = color 13 | { 14 | return "\u{1B}[38;2;\(color.r);\(color.g);\(color.b)m" 15 | } 16 | else 17 | { 18 | return "\u{1B}[39m" 19 | } 20 | } 21 | public 22 | static func bg(_ color:(r:UInt8, g:UInt8, b:UInt8)?) -> String 23 | { 24 | if let color:(r:UInt8, g:UInt8, b:UInt8) = color 25 | { 26 | return "\u{1B}[48;2;\(color.r);\(color.g);\(color.b)m" 27 | } 28 | else 29 | { 30 | return "\u{1B}[49m" 31 | } 32 | } 33 | 34 | public 35 | static func quantize(_ color:(r:F, g:F, b:F)) -> (r:UInt8, g:UInt8, b:UInt8) 36 | where F:BinaryFloatingPoint 37 | { 38 | let r:UInt8 = .init((.init(UInt8.max) * max(0, min(color.r, 1))).rounded()), 39 | g:UInt8 = .init((.init(UInt8.max) * max(0, min(color.g, 1))).rounded()), 40 | b:UInt8 = .init((.init(UInt8.max) * max(0, min(color.b, 1))).rounded()) 41 | return (r, g, b) 42 | } 43 | public 44 | static func color(_ string:String, _ color:(r:F, g:F, b:F)) -> String 45 | where F:BinaryFloatingPoint 46 | { 47 | return Self.color(string, Self.quantize(color)) 48 | } 49 | public 50 | static func color(_ string:String, _ fg:(r:UInt8, g:UInt8, b:UInt8)) -> String 51 | { 52 | return "\(Self.fg(fg))\(string)\(Self.fg(nil))" 53 | } 54 | 55 | public 56 | static func highlight(_ string:String, _ color:(r:F, g:F, b:F)) -> String 57 | where F:BinaryFloatingPoint 58 | { 59 | return Self.highlight(string, Self.quantize(color)) 60 | } 61 | public 62 | static func highlight(_ string:String, _ bg:(r:UInt8, g:UInt8, b:UInt8)) -> String 63 | { 64 | let fg:(r:UInt8, g:UInt8, b:UInt8) = 65 | (bg.r / 3 + bg.g / 3 + bg.b / 3) < 128 ? (.max, .max, .max) : (0, 0, 0) 66 | 67 | return "\(Self.bg(bg))\(Self.fg(fg))\(string)\(Self.fg(nil))\(Self.bg(nil))" 68 | } 69 | public 70 | static func swatch(_ color:(r:F, g:F, b:F)) -> String 71 | where F:BinaryFloatingPoint 72 | { 73 | let v:(String, String, String) = 74 | ( 75 | String.pad("\(color.r)", left: 3), 76 | String.pad("\(color.g)", left: 3), 77 | String.pad("\(color.b)", left: 3) 78 | ) 79 | return Self.highlight(" \(v.0)\(v.1)\(v.2) ", color) 80 | } 81 | public 82 | static func square(_ color:(r:F, g:F, b:F)) -> String 83 | where F:BinaryFloatingPoint 84 | { 85 | return Self.highlight(" ", color) 86 | } 87 | public 88 | static func square(_ color:(r:UInt8, g:UInt8, b:UInt8)) -> String 89 | { 90 | return Self.highlight(" ", color) 91 | } 92 | 93 | public 94 | static func bits(_ x:I) -> String where I:FixedWidthInteger 95 | { 96 | return (0 ..< I.bitWidth).reversed().map 97 | { 98 | (x >> $0) & 1 == 0 ? Self.highlight("0", (0.2, 0.2, 0.2)) : Self.highlight("1", (1, 1, 1)) 99 | }.joined(separator: "") 100 | } 101 | 102 | public 103 | static func print(_ string:String, highlight color:(r:F, g:F, b:F)) 104 | where F:BinaryFloatingPoint 105 | { 106 | Swift.print(Self.highlight(string, color)) 107 | } 108 | public 109 | static func print(_ string:String, color:(r:F, g:F, b:F)) 110 | where F:BinaryFloatingPoint 111 | { 112 | Swift.print(Self.color(string, color)) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Snippets/DecodeAdvanced.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | extension String 5 | { 6 | static 7 | func pad(_ string:String, left count:Int) -> Self 8 | { 9 | .init(repeating: " ", count: count - string.count) + string 10 | } 11 | static 12 | func pad(_ string:String, right count:Int) -> Self 13 | { 14 | string + .init(repeating: " ", count: count - string.count) 15 | } 16 | } 17 | 18 | let path:String = "examples/decode-advanced/karlie-2019.jpg" 19 | guard let spectral:JPEG.Data.Spectral = try .decompress(path: path) 20 | else 21 | { 22 | fatalError("failed to open file '\(path)'") 23 | } 24 | 25 | print(""" 26 | '\(path)' (\(spectral.layout.format)) 27 | { 28 | size : (\(spectral.size.x), \(spectral.size.y)) 29 | process : \(spectral.layout.process) 30 | precision : \(spectral.layout.format.precision) 31 | components : 32 | [ 33 | \(spectral.layout.residents.sorted(by: { $0.key < $1.key }).map 34 | { 35 | let (component, qi):(JPEG.Component, JPEG.Table.Quantization.Key) = 36 | spectral.layout.planes[$0.value] 37 | return "\($0.key): (\(component.factor.x), \(component.factor.y), qi: \(qi))" 38 | }.joined(separator: "\n ")) 39 | ] 40 | scans : 41 | [ 42 | \(spectral.layout.scans.map 43 | { 44 | "[band: \($0.band), bits: \($0.bits)]: \($0.components.map(\.ci))" 45 | }.joined(separator: "\n ")) 46 | ] 47 | } 48 | """) 49 | 50 | for metadata:JPEG.Metadata in spectral.metadata 51 | { 52 | switch metadata 53 | { 54 | case .application(let a, data: let data): 55 | Swift.print("metadata (application \(a), \(data.count) bytes)") 56 | case .comment(data: let data): 57 | Swift.print(""" 58 | comment 59 | { 60 | '\(String.init(decoding: data, as: Unicode.UTF8.self))' 61 | } 62 | """) 63 | case .jfif(let jfif): 64 | Swift.print(jfif) 65 | case .exif(let exif): 66 | Swift.print(exif) 67 | if let (type, count, box):(JPEG.EXIF.FieldType, Int, JPEG.EXIF.Box) = exif[tag: 315], 68 | case .ascii = type 69 | { 70 | let artist:String = .init(decoding: (0 ..< count).map 71 | { 72 | exif[box.asOffset + $0, as: UInt8.self] 73 | }, as: Unicode.ASCII.self) 74 | print("artist: \(artist)") 75 | } 76 | } 77 | } 78 | 79 | let keys:Set = .init(spectral.layout.planes.map(\.qi)) 80 | for qi:JPEG.Table.Quantization.Key in keys.sorted() 81 | { 82 | let q:Int = spectral.quanta.index(forKey: qi) 83 | let table:JPEG.Table.Quantization = spectral.quanta[q] 84 | print("quantization table \(qi):") 85 | print(""" 86 | ┌ \(String.init(repeating: " ", count: 4 * 8)) ┐ 87 | \((0 ..< 8).map 88 | { 89 | (h:Int) in 90 | """ 91 | │ \((0 ..< 8).map 92 | { 93 | (k:Int) in 94 | String.pad("\(table[z: JPEG.Table.Quantization.z(k: k, h: h)]) ", left: 4) 95 | }.joined()) │ 96 | """ 97 | }.joined(separator: "\n")) 98 | └ \(String.init(repeating: " ", count: 4 * 8)) ┘ 99 | """) 100 | } 101 | 102 | 103 | let planar:JPEG.Data.Planar = spectral.idct() 104 | for (p, plane):(Int, JPEG.Data.Planar.Plane) in planar.enumerated() 105 | { 106 | print(""" 107 | plane \(p) 108 | { 109 | size: (\(plane.size.x), \(plane.size.y)) 110 | } 111 | """) 112 | 113 | let samples:[UInt8] = plane.indices.map 114 | { 115 | (i:(x:Int, y:Int)) in 116 | .init(clamping: plane[x: i.x, y: i.y]) 117 | } 118 | 119 | let planepath:String = "\(path)-\(p).\(plane.size.x)x\(plane.size.y).gray" 120 | guard let _:Void = (System.File.Destination.open(path: planepath) 121 | { 122 | guard let _:Void = $0.write(samples) 123 | else 124 | { 125 | fatalError("failed to write to file '\(planepath)'") 126 | } 127 | }) 128 | else 129 | { 130 | fatalError("failed to open file '\(planepath)'") 131 | } 132 | } 133 | 134 | let rectangular:JPEG.Data.Rectangular = planar.interleaved(cosite: false) 135 | let rgb:[JPEG.RGB] = rectangular.unpack(as: JPEG.RGB.self) 136 | guard let _:Void = (System.File.Destination.open(path: "\(path).rgb") 137 | { 138 | guard let _:Void = $0.write(rgb.flatMap{ [$0.r, $0.g, $0.b] }) 139 | else 140 | { 141 | fatalError("failed to write to file '\(path).rgb'") 142 | } 143 | }) 144 | else 145 | { 146 | fatalError("failed to open file '\(path).rgb'") 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ***`jpeg`*** 4 | 5 | [![Tests](https://github.com/tayloraswift/swift-jpeg/actions/workflows/Tests.yml/badge.svg)](https://github.com/tayloraswift/swift-jpeg/actions/workflows/Tests.yml) 6 | [![Documentation](https://github.com/tayloraswift/swift-jpeg/actions/workflows/Documentation.yml/badge.svg)](https://github.com/tayloraswift/swift-jpeg/actions/workflows/Documentation.yml) 7 | 8 |
9 | 10 | 11 | Swift *JPEG* is a cross-platform pure Swift framework for decoding, inspecting, editing, and encoding JPEG images. The core framework has no external dependencies, including *Foundation*, and should compile and provide consistent behavior on *all* Swift platforms. The framework supports additional features, such as file system support, on Linux and MacOS. 12 | 13 | Swift *JPEG* is available under the [Apache 2.0 License](LICENSE). The [example programs](Snippets/) are public domain and can be adapted freely. 14 | 15 |
16 | 17 | [documentation](https://swiftinit.org/docs/swift-jpeg/jpeg) · 18 | [license](LICENSE) 19 | 20 |
21 | 22 | 23 | ## Requirements 24 | 25 | The swift-jpeg library requires Swift 5.10 or later. 26 | 27 | 28 | | Platform | Status | 29 | | -------- | ------ | 30 | | 🐧 Linux | [![Tests](https://github.com/tayloraswift/swift-jpeg/actions/workflows/Tests.yml/badge.svg)](https://github.com/tayloraswift/swift-jpeg/actions/workflows/Tests.yml) | 31 | | 🍏 Darwin | [![Tests](https://github.com/tayloraswift/swift-jpeg/actions/workflows/Tests.yml/badge.svg)](https://github.com/tayloraswift/swift-jpeg/actions/workflows/Tests.yml) | 32 | | 🍏 Darwin (iOS) | [![iOS](https://github.com/tayloraswift/swift-jpeg/actions/workflows/iOS.yml/badge.svg)](https://github.com/tayloraswift/swift-jpeg/actions/workflows/iOS.yml) | 33 | | 🍏 Darwin (tvOS) | [![tvOS](https://github.com/tayloraswift/swift-jpeg/actions/workflows/tvOS.yml/badge.svg)](https://github.com/tayloraswift/swift-jpeg/actions/workflows/tvOS.yml) | 34 | | 🍏 Darwin (visionOS) | [![visionOS](https://github.com/tayloraswift/swift-jpeg/actions/workflows/visionOS.yml/badge.svg)](https://github.com/tayloraswift/swift-jpeg/actions/workflows/visionOS.yml) | 35 | | 🍏 Darwin (watchOS) | [![watchOS](https://github.com/tayloraswift/swift-jpeg/actions/workflows/watchOS.yml/badge.svg)](https://github.com/tayloraswift/swift-jpeg/actions/workflows/watchOS.yml) | 36 | 37 | 38 | [Check deployment minimums](https://swiftinit.org/docs/swift-jpeg#ss:platform-requirements) 39 | 40 | 41 | ## [tutorials and example programs](examples/) 42 | 43 | * [basic decoding](examples#basic-decoding) 44 | * [basic encoding](examples#basic-encoding) 45 | * [advanced decoding](examples#advanced-decoding) 46 | * [advanced encoding](examples#advanced-encoding) 47 | * [using in-memory images](examples#using-in-memory-images) 48 | * [online decoding](examples#online-decoding) 49 | * [requantizing images](examples#requantizing-images) 50 | * [lossless rotations](examples#lossless-rotations) 51 | * [custom color formats](examples#custom-color-formats) 52 | 53 | ## [api reference](https://swiftinit.org/docs/swift-jpeg/jpeg/) 54 | 55 | * [`JPEG.JPEG`](https://swiftinit.org/docs/swift-jpeg/jpeg/jpeg) 56 | * [`JPEG.General`](https://swiftinit.org/docs/swift-jpeg/jpeg/general) 57 | * [`JPEG.System`](https://swiftinit.org/docs/swift-jpeg/jpegsystem/system) 58 | 59 | ## getting started 60 | 61 | To Swift *JPEG* in a project, add this descriptor to the `dependencies` list in your `Package.swift`: 62 | 63 | ```swift 64 | .package(url: "https://github.com/tayloraswift/swift-jpeg", from: "2.0.0") 65 | ``` 66 | 67 | ## basic usage 68 | 69 | Decode an image: 70 | 71 | ```swift 72 | import JPEG 73 | func decode(jpeg path:String) throws 74 | { 75 | guard let image:JPEG.Data.Rectangular = try .decompress(path: path) 76 | else 77 | { 78 | // failed to access file from file system 79 | } 80 | 81 | let rgb:[JPEG.RGB] = image.unpack(as: JPEG.RGB.self), 82 | size:(x:Int, y:Int) = image.size 83 | // ... 84 | } 85 | ``` 86 | 87 | Encode an image: 88 | 89 | ```swift 90 | import JPEG 91 | func encode(jpeg path:String, size:(x:Int, y:Int), pixels:[JPEG.RGB], 92 | compression:Double) // 0.0 = highest quality 93 | throws 94 | { 95 | let layout:JPEG.Layout = .init( 96 | format: .ycc8, 97 | process: .baseline, 98 | components: 99 | [ 100 | 1: (factor: (2, 2), qi: 0), // Y 101 | 2: (factor: (1, 1), qi: 1), // Cb 102 | 3: (factor: (1, 1), qi: 1), // Cr 103 | ], 104 | scans: 105 | [ 106 | .sequential((1, \.0, \.0), (2, \.1, \.1), (3, \.1, \.1)), 107 | ]) 108 | let jfif:JPEG.JFIF = .init(version: .v1_2, density: (72, 72, .inches)) 109 | let image:JPEG.Data.Rectangular = 110 | .pack(size: size, layout: layout, metadata: [.jfif(jfif)], pixels: rgb) 111 | 112 | try image.compress(path: path, quanta: 113 | [ 114 | 0: JPEG.CompressionLevel.luminance( compression).quanta, 115 | 1: JPEG.CompressionLevel.chrominance(compression).quanta 116 | ]) 117 | } 118 | ``` 119 | -------------------------------------------------------------------------------- /Sources/JPEG/debug.swift: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | extension JPEG.Component.Key:ExpressibleByIntegerLiteral 6 | { 7 | public 8 | init(integerLiteral:UInt8) 9 | { 10 | self.init(integerLiteral) 11 | } 12 | } 13 | extension JPEG.Table.Quantization.Key:ExpressibleByIntegerLiteral 14 | { 15 | public 16 | init(integerLiteral:Int) 17 | { 18 | self.init(integerLiteral) 19 | } 20 | } 21 | 22 | // print descriptions 23 | extension String 24 | { 25 | public 26 | init(selector:WritableKeyPath<(Delegate?, Delegate?, Delegate?, Delegate?), Delegate?>) 27 | { 28 | switch selector 29 | { 30 | case \.0: 31 | self = "0" 32 | case \.1: 33 | self = "1" 34 | case \.2: 35 | self = "2" 36 | case \.3: 37 | self = "3" 38 | 39 | default: 40 | self = "" 41 | } 42 | } 43 | } 44 | 45 | extension JPEG.Process:CustomStringConvertible 46 | { 47 | public 48 | var description:String 49 | { 50 | switch self 51 | { 52 | case .baseline: 53 | return "baseline sequential DCT" 54 | case .extended(coding: let coding, differential: let differential): 55 | return "extended sequential DCT (\(coding), \(differential ? "differential" : "non-differential"))" 56 | case .progressive(coding: let coding, differential: let differential): 57 | return "progressive DCT (\(coding), \(differential ? "differential" : "non-differential"))" 58 | case .lossless(coding: let coding, differential: let differential): 59 | return "lossless process (\(coding), \(differential ? "differential" : "non-differential"))" 60 | } 61 | } 62 | } 63 | 64 | extension JPEG.Component:CustomStringConvertible 65 | { 66 | public 67 | var description:String 68 | { 69 | return "{quantization table: \(String.init(selector: self.selector)), sample factors: (\(self.factor.x), \(self.factor.y))}" 70 | } 71 | } 72 | extension JPEG.Scan.Component:CustomStringConvertible 73 | { 74 | public 75 | var description:String 76 | { 77 | "{dc huffman table: \(String.init(selector: self.selector.dc)), ac huffman table: \(String.init(selector: self.selector.ac))}" 78 | } 79 | } 80 | extension JPEG.Component.Key:CustomStringConvertible 81 | { 82 | public 83 | var description:String 84 | { 85 | "[\(self.value)]" 86 | } 87 | } 88 | extension JPEG.Table.Quantization.Key:CustomStringConvertible 89 | { 90 | public 91 | var description:String 92 | { 93 | "[\(self.value)]" 94 | } 95 | } 96 | 97 | extension JPEG.Header.Frame:CustomStringConvertible 98 | { 99 | public 100 | var description:String 101 | { 102 | """ 103 | frame header: 104 | { 105 | mode : \(self.process), 106 | precision : \(self.precision), 107 | initial size : (\(self.size.x), \(self.size.y)), 108 | components : 109 | [ 110 | \(self.components.sorted(by: { $0.key < $1.key }).map 111 | { 112 | return "[\($0.key)]: \($0.value)" 113 | }.joined(separator: ", \n ")) 114 | ] 115 | } 116 | """ 117 | } 118 | } 119 | 120 | extension JPEG.Header.Scan:CustomStringConvertible 121 | { 122 | public 123 | var description:String 124 | { 125 | """ 126 | scan header (\(Self.self)): 127 | { 128 | band : \(self.band.lowerBound) ..< \(self.band.upperBound), 129 | bits : \(self.bits.lowerBound) ..< \(self.bits.upperBound), 130 | components : 131 | [ 132 | \(self.components.map 133 | { 134 | "[\($0.ci)]: \($0)" 135 | }.joined(separator: ", \n ")) 136 | ] 137 | } 138 | """ 139 | } 140 | } 141 | 142 | extension JPEG.Table.Huffman:CustomStringConvertible 143 | { 144 | public 145 | var description:String 146 | { 147 | """ 148 | huffman table (\(Self.self)) 149 | { 150 | target : \(String.init(selector: self.target)) 151 | } 152 | """ 153 | } 154 | } 155 | 156 | extension JPEG.Table.Quantization:CustomStringConvertible 157 | { 158 | public 159 | var description:String 160 | { 161 | """ 162 | quantization table (\(Self.self)) 163 | { 164 | target : \(String.init(selector: self.target)) 165 | } 166 | """ 167 | } 168 | } 169 | 170 | extension JPEG.JFIF:CustomStringConvertible 171 | { 172 | public 173 | var description:String 174 | { 175 | """ 176 | metadata (\(Self.self)) 177 | { 178 | version : \(self.version) 179 | unit : \(self.density.unit.map(String.init(describing:)) ?? "none") 180 | density : (\(self.density.x), \(self.density.y)) 181 | } 182 | """ 183 | } 184 | } 185 | extension JPEG.EXIF:CustomStringConvertible 186 | { 187 | public 188 | var description:String 189 | { 190 | """ 191 | metadata (\(Self.self)) 192 | { 193 | endianness : \(self.endianness) 194 | storage : \(self.storage.count) bytes 195 | } 196 | """ 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/regression/tests.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | extension Test 5 | { 6 | static 7 | var cases:[(name:String, function:Function)] 8 | { 9 | [ 10 | ("color-sequential-regression", .string(Self.test(_:), 11 | [ 12 | "tests/regression/gold/color-sequential-1.jpg", 13 | "tests/regression/gold/color-sequential-2.jpg", 14 | "tests/regression/gold/color-sequential-3.jpg", 15 | "tests/regression/gold/color-sequential-4.jpg", 16 | ])), 17 | ("grayscale-sequential-regression", .string(Self.test(_:), 18 | [ 19 | "tests/regression/gold/grayscale-sequential-1.jpg", 20 | "tests/regression/gold/grayscale-sequential-2.jpg", 21 | ])), 22 | ("color-progressive-regression", .string(Self.test(_:), 23 | [ 24 | "tests/regression/gold/color-progressive-1.jpg", 25 | "tests/regression/gold/color-progressive-2.jpg", 26 | "tests/regression/gold/color-progressive-3.jpg", 27 | "tests/regression/gold/color-progressive-4.jpg", 28 | ])), 29 | ("grayscale-progressive-regression", .string(Self.test(_:), 30 | [ 31 | "tests/regression/gold/grayscale-progressive-1.jpg", 32 | "tests/regression/gold/grayscale-progressive-2.jpg", 33 | ])), 34 | ] 35 | } 36 | 37 | // this test attempts to decode the given image, and compares it to the golden 38 | // outputs in the same directory 39 | static 40 | func test(_ path:String) -> Result 41 | { 42 | do 43 | { 44 | guard let image:JPEG.Data.Rectangular = try .decompress(path: path) 45 | else 46 | { 47 | throw Failure.init(message: "failed to open file '\(path)'") 48 | } 49 | 50 | let output:(ycc:[JPEG.YCbCr], rgb:[JPEG.RGB]) = 51 | ( 52 | image.unpack(as: JPEG.YCbCr.self), 53 | image.unpack(as: JPEG.RGB.self) 54 | ) 55 | guard let ycc:[JPEG.YCbCr] = try (System.File.Source.open(path: "\(path).ycc") 56 | { 57 | guard let data:[UInt8] = $0.read(count: 3 * output.ycc.count) 58 | else 59 | { 60 | throw Failure.init(message: "failed to read from file '\(path).ycc'") 61 | } 62 | 63 | return (0 ..< output.ycc.count).map 64 | { 65 | (i:Int) -> JPEG.YCbCr in 66 | .init(y: data[i * 3], cb: data[i * 3 + 1], cr: data[i * 3 + 2]) 67 | } 68 | }), 69 | let rgb:[JPEG.RGB] = try (System.File.Source.open(path: "\(path).rgb") 70 | { 71 | guard let data:[UInt8] = $0.read(count: 3 * output.rgb.count) 72 | else 73 | { 74 | throw Failure.init(message: "failed to read from file '\(path).rgb'") 75 | } 76 | 77 | return (0 ..< output.rgb.count).map 78 | { 79 | (i:Int) -> JPEG.RGB in 80 | .init(data[i * 3], data[i * 3 + 1], data[i * 3 + 2]) 81 | } 82 | }) 83 | else 84 | { 85 | // write new golden output if there is none at the given location 86 | guard let _:Void = try (System.File.Destination.open(path: "\(path).ycc") 87 | { 88 | guard let _:Void = $0.write(output.ycc.flatMap{ [$0.y, $0.cb, $0.cr] }) 89 | else 90 | { 91 | throw Failure.init(message: "failed to write to file '\(path).ycc'") 92 | } 93 | }) 94 | else 95 | { 96 | throw Failure.init(message: "failed to open file '\(path).ycc'") 97 | } 98 | 99 | guard let _:Void = try (System.File.Destination.open(path: "\(path).rgb") 100 | { 101 | guard let _:Void = $0.write(output.rgb.flatMap{ [$0.r, $0.g, $0.b] }) 102 | else 103 | { 104 | throw Failure.init(message: "failed to write to file '\(path).rgb'") 105 | } 106 | }) 107 | else 108 | { 109 | throw Failure.init(message: "failed to open file '\(path).rgb'") 110 | } 111 | 112 | throw Failure.init(message: 113 | "no golden output for '\(path)' (new golden output written to '\(path).ycc', '\(path).rgb')") 114 | } 115 | 116 | guard output.ycc == ycc, output.rgb == rgb 117 | else 118 | { 119 | throw Failure.init(message: "decoded output does not match golden output") 120 | } 121 | 122 | return .success(()) 123 | } 124 | catch 125 | { 126 | if let error:Failure = error as? Failure 127 | { 128 | return .failure(error) 129 | } 130 | else if let error:JPEG.Error = error as? JPEG.Error 131 | { 132 | return .failure(.init(message: error.message)) 133 | } 134 | else 135 | { 136 | return .failure(.init(message: "\(error)")) 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/compare/main.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGInspection 3 | import JPEGSystem 4 | 5 | func discrepancy(jpeg:String, reference:String) 6 | throws -> (average:Double, max:Double) 7 | { 8 | guard let rectangular:JPEG.Data.Rectangular = 9 | try .decompress(path: jpeg) 10 | else 11 | { 12 | fatalError("failed to open file '\(jpeg)'") 13 | } 14 | 15 | let output:[JPEG.YCbCr] = rectangular.unpack(as: JPEG.YCbCr.self) 16 | guard let expected:[JPEG.YCbCr] = (System.File.Source.open(path: reference) 17 | { 18 | guard let data:[UInt8] = $0.read(count: 3 * output.count) 19 | else 20 | { 21 | fatalError("failed to read file '\(reference)'") 22 | } 23 | 24 | return (0 ..< output.count).map 25 | { 26 | let y:UInt8 = data[$0 * 3 ], 27 | cb:UInt8 = data[$0 * 3 + 1], 28 | cr:UInt8 = data[$0 * 3 + 2] 29 | return .init(y: y, cb: cb, cr: cr) 30 | } 31 | }) 32 | else 33 | { 34 | fatalError("failed to open file '\(reference)'") 35 | } 36 | 37 | // terminal output 38 | for i:Int in 0 ..< rectangular.size.y 39 | { 40 | func gradientRed(_ x:Double) -> (r:Double, g:Double, b:Double) 41 | { 42 | (x, 2 * (x - 0.5), 1 * (x - 0.4)) 43 | } 44 | func gradientBlue(_ x:Double) -> (r:Double, g:Double, b:Double) 45 | { 46 | (0.8 * (x - 0.4), 0, x) 47 | } 48 | 49 | let line1:String = (0 ..< rectangular.size.x).map 50 | { 51 | (j:Int) in 52 | 53 | let c:JPEG.RGB = output[j + i * rectangular.size.x].rgb 54 | return Highlight.square((c.r, c.g, c.b)) 55 | }.joined(separator: "") 56 | let line2:String = (0 ..< rectangular.size.x).map 57 | { 58 | (j:Int) in 59 | 60 | let c:JPEG.RGB = expected[j + i * rectangular.size.x].rgb 61 | return Highlight.square((c.r, c.g, c.b)) 62 | }.joined(separator: "") 63 | let line3:String = (0 ..< rectangular.size.x).map 64 | { 65 | (j:Int) in 66 | 67 | let y:(UInt8, UInt8) = 68 | ( 69 | output[j + i * rectangular.size.x].y, 70 | expected[j + i * rectangular.size.x].y 71 | ) 72 | 73 | let d:Int = Int.init(y.0) - Int.init(y.1) 74 | if d < 0 75 | { 76 | return Highlight.square(gradientBlue(.init(-d) / 10)) 77 | } 78 | else 79 | { 80 | return Highlight.square(gradientRed(.init(d) / 10)) 81 | } 82 | }.joined(separator: "") 83 | print("\(line1) \(line2) \(line3)") 84 | } 85 | for i:Int in 0 ..< rectangular.size.y 86 | { 87 | for j:Int in 0 ..< rectangular.size.x 88 | { 89 | let y:(UInt8, UInt8) = 90 | ( 91 | output[j + i * rectangular.size.x].y, 92 | expected[j + i * rectangular.size.x].y 93 | ) 94 | 95 | if abs(Int.init(y.0) - Int.init(y.1)) > 1 96 | { 97 | print("output = \(y.0), expected = \(y.1)") 98 | } 99 | } 100 | } 101 | 102 | var total:Int = 0, 103 | max:Int = 0 104 | 105 | for (a, b):(JPEG.YCbCr, JPEG.YCbCr) in zip(output, expected) 106 | { 107 | let difference:Int = abs(.init(a.y) - .init(b.y)) 108 | 109 | total += difference 110 | max = Swift.max(max, difference) 111 | } 112 | 113 | return (average: .init(total) / .init(output.count), max: .init(max)) 114 | } 115 | 116 | func bin(_ value:Double, into histogram:inout [Int], range:Range) 117 | { 118 | let u:Double = (value - .init(range.lowerBound)) / 119 | .init(range.count) * .init(histogram.count) 120 | let b:Int = max(histogram.startIndex, min(.init(u), histogram.endIndex - 1)) 121 | histogram[b] += 1 122 | } 123 | 124 | func print(histogram:[Int], range:Range, width:Int) 125 | { 126 | func gradient(_ x:Double) -> (r:Double, g:Double, b:Double) 127 | { 128 | (1, 1 - 1.5 * (x - 0.4), 1 - x) 129 | } 130 | 131 | let max:Int = histogram.max() ?? 1 132 | for (i, count):(Int, Int) in zip(histogram.indices, histogram) 133 | { 134 | let a:(Double, Double) = 135 | ( 136 | .init(range.lowerBound), 137 | .init(range.upperBound) 138 | ) 139 | let u:(Double, Double) = 140 | ( 141 | .init(i ) / .init(histogram.count), 142 | .init(i + 1) / .init(histogram.count) 143 | ) 144 | let x:(Double, Double) = 145 | ( 146 | a.0 * (1 - u.0) + a.1 * u.0, 147 | a.0 * (1 - u.1) + a.1 * u.1 148 | ) 149 | 150 | let rgb:(r:Double, g:Double, b:Double) = gradient(u.0) 151 | 152 | let left:String = Highlight.highlight( 153 | " \(String.pad("\(x.0)", left: 8)) ..< \(String.pad("\(x.1)", left: 8))", rgb) 154 | let label:String = .pad("\(count)", left: 5) 155 | let right:String = .init(repeating: "█", count: width * count / max) 156 | 157 | print("\(left) \(label) \(right)") 158 | } 159 | } 160 | 161 | let range:(average:Range, max:Range) = 162 | ( 163 | 0 ..< 1, 164 | 0 ..< 32 165 | ) 166 | var histogram:(average:[Int], max:[Int]) = 167 | ( 168 | .init(repeating: 0, count: 64), 169 | .init(repeating: 0, count: 32) 170 | ) 171 | for argument:String in CommandLine.arguments.dropFirst() 172 | { 173 | let (average, max):(Double, Double) = 174 | try discrepancy(jpeg: argument, reference: "\(argument).ycc") 175 | bin(average, into: &histogram.average, range: range.average) 176 | bin(max, into: &histogram.max, range: range.max) 177 | } 178 | print("average discrepancy") 179 | print(histogram: histogram.average, range: range.average, width: 80) 180 | print("maximum discrepancy") 181 | print(histogram: histogram.max, range: range.max, width: 80) 182 | -------------------------------------------------------------------------------- /Snippets/CustomColor.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | extension JPEG 5 | { 6 | enum Deep 7 | { 8 | case rgba12 9 | } 10 | 11 | struct RGB12 12 | { 13 | var r:UInt16 14 | var g:UInt16 15 | var b:UInt16 16 | 17 | init(_ r:UInt16, _ g:UInt16, _ b:UInt16) 18 | { 19 | self.r = r 20 | self.g = g 21 | self.b = b 22 | } 23 | } 24 | 25 | struct RGBA12 26 | { 27 | var r:UInt16 28 | var g:UInt16 29 | var b:UInt16 30 | var a:UInt16 31 | 32 | init(_ r:UInt16, _ g:UInt16, _ b:UInt16, _ a:UInt16) 33 | { 34 | self.r = r 35 | self.g = g 36 | self.b = b 37 | self.a = a 38 | } 39 | } 40 | } 41 | 42 | extension JPEG.Deep:JPEG.Format 43 | { 44 | static 45 | func recognize(_ components:Set, precision:Int) -> Self? 46 | { 47 | switch (components.sorted(), precision) 48 | { 49 | case ([4, 5, 6, 7], 12): 50 | return .rgba12 51 | default: 52 | return nil 53 | } 54 | } 55 | 56 | // the ordering here is used to determine planar indices 57 | var components:[JPEG.Component.Key] 58 | { 59 | [4, 5, 6, 7] 60 | } 61 | var precision:Int 62 | { 63 | 12 64 | } 65 | } 66 | extension JPEG.RGB12:JPEG.Color 67 | { 68 | static 69 | func unpack(_ interleaved:[UInt16], of format:JPEG.Deep) -> [Self] 70 | { 71 | switch format 72 | { 73 | case .rgba12: 74 | return stride(from: interleaved.startIndex, to: interleaved.endIndex, by: 4).map 75 | { 76 | (base:Int) -> Self in 77 | .init( 78 | interleaved[base ], 79 | interleaved[base + 1], 80 | interleaved[base + 2]) 81 | } 82 | } 83 | } 84 | static 85 | func pack(_ pixels:[Self], as format:JPEG.Deep) -> [UInt16] 86 | { 87 | switch format 88 | { 89 | case .rgba12: 90 | return pixels.flatMap 91 | { 92 | [min($0.r, 0x0fff), min($0.g, 0x0fff), min($0.b, 0x0fff), 0x0fff] 93 | } 94 | } 95 | } 96 | } 97 | extension JPEG.RGBA12:JPEG.Color 98 | { 99 | static 100 | func unpack(_ interleaved:[UInt16], of format:JPEG.Deep) -> [Self] 101 | { 102 | switch format 103 | { 104 | case .rgba12: 105 | return stride(from: interleaved.startIndex, to: interleaved.endIndex, by: 4).map 106 | { 107 | (base:Int) -> Self in 108 | .init( 109 | interleaved[base ], 110 | interleaved[base + 1], 111 | interleaved[base + 2], 112 | interleaved[base + 3]) 113 | } 114 | } 115 | } 116 | static 117 | func pack(_ pixels:[Self], as format:JPEG.Deep) -> [UInt16] 118 | { 119 | switch format 120 | { 121 | case .rgba12: 122 | return pixels.flatMap 123 | { 124 | [min($0.r, 0x0fff), min($0.g, 0x0fff), min($0.b, 0x0fff), min($0.a, 0x0fff)] 125 | } 126 | } 127 | } 128 | } 129 | 130 | 131 | func sin(_ x:Double) -> UInt16 132 | { 133 | .init(0x0fff * (_sin(2.0 * .pi * x) * 0.5 + 0.5)) 134 | } 135 | let gradient:[JPEG.RGBA12] = stride(from: 0.0, to: 1.0, by: 0.005).flatMap 136 | { 137 | (phase:Double) -> [JPEG.RGBA12] in 138 | stride(from: 0.0, to: 1.0, by: 0.001).map 139 | { 140 | .init(sin(phase + $0 - 0.15), sin(phase + $0), sin(phase + $0 + 0.15), 0x0fff) 141 | } 142 | } 143 | 144 | let format:JPEG.Deep = .rgba12 145 | let R:JPEG.Component.Key = format.components[0], 146 | G:JPEG.Component.Key = format.components[1], 147 | B:JPEG.Component.Key = format.components[2], 148 | A:JPEG.Component.Key = format.components[3] 149 | 150 | let layout:JPEG.Layout = .init( 151 | format: format, 152 | process: .progressive(coding: .huffman, differential: false), 153 | components: 154 | [ 155 | R: (factor: (2, 2), qi: 0), 156 | G: (factor: (2, 2), qi: 0), 157 | B: (factor: (2, 2), qi: 0), 158 | A: (factor: (1, 1), qi: 1), 159 | ], 160 | scans: 161 | [ 162 | .progressive((G, \.0), (A, \.1), bits: 0...), 163 | .progressive((R, \.0), (B, \.1), bits: 0...), 164 | 165 | .progressive((R, \.0), band: 1 ..< 64, bits: 1...), 166 | .progressive((G, \.0), band: 1 ..< 64, bits: 1...), 167 | .progressive((B, \.0), band: 1 ..< 64, bits: 1...), 168 | .progressive((A, \.0), band: 1 ..< 64, bits: 1...), 169 | 170 | .progressive((R, \.0), band: 1 ..< 64, bit: 0), 171 | .progressive((G, \.0), band: 1 ..< 64, bit: 0), 172 | .progressive((B, \.0), band: 1 ..< 64, bit: 0), 173 | .progressive((A, \.0), band: 1 ..< 64, bit: 0), 174 | ]) 175 | 176 | let path:String = "examples/custom-color/output.jpg" 177 | let image:JPEG.Data.Rectangular = 178 | .pack(size: (1000, 200), layout: layout, metadata: [], pixels: gradient) 179 | try image.compress(path: path, quanta: 180 | [ 181 | 0: [1, 2, 2, 3, 3, 3] + .init(repeating: 10, count: 58), 182 | 1: [1] + .init(repeating: 100, count: 63), 183 | ]) 184 | 185 | guard let saved:JPEG.Data.Rectangular = try .decompress(path: path) 186 | else 187 | { 188 | fatalError("failed to open file '\(path)'") 189 | } 190 | 191 | let rgb12:[JPEG.RGB12] = image.unpack(as: JPEG.RGB12.self) 192 | guard let _:Void = (System.File.Destination.open(path: "\(path).rgb") 193 | { 194 | guard let _:Void = $0.write(rgb12.flatMap 195 | { 196 | [ 197 | .init($0.r >> 4 as UInt16), .init(($0.r << 4 as UInt16) & 0xff), 198 | .init($0.g >> 4 as UInt16), .init(($0.g << 4 as UInt16) & 0xff), 199 | .init($0.b >> 4 as UInt16), .init(($0.b << 4 as UInt16) & 0xff), 200 | ] 201 | }) 202 | else 203 | { 204 | fatalError("failed to write to file '\(path).rgb'") 205 | } 206 | }) 207 | else 208 | { 209 | fatalError("failed to open file '\(path).rgb'") 210 | } 211 | -------------------------------------------------------------------------------- /tests/fuzz/main.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGInspection 3 | import JPEGSystem 4 | 5 | func fuzz(rng:inout RNG, path:String) throws where RNG:RandomNumberGenerator 6 | { 7 | let format:JPEG.Common = .ycc8 8 | let Y:JPEG.Component.Key = format.components[0], 9 | Cb:JPEG.Component.Key = format.components[1], 10 | Cr:JPEG.Component.Key = format.components[2] 11 | 12 | let layout:JPEG.Layout = .init( 13 | format: format, 14 | process: .progressive(coding: .huffman, differential: false), 15 | components: 16 | [ 17 | Y: (factor: (1, 1), qi: 0), 18 | Cb: (factor: (1, 1), qi: 1), 19 | Cr: (factor: (1, 1), qi: 1), 20 | ], 21 | scans: 22 | [ 23 | .progressive((Y, \.0), (Cb, \.1), (Cr, \.1), bits: 2...), 24 | .progressive( Y, Cb, Cr , bit: 1 ), 25 | .progressive( Y, Cb, Cr , bit: 0 ), 26 | 27 | .progressive((Y, \.0), band: 1 ..< 64, bits: 1...), 28 | 29 | .progressive((Cb, \.0), band: 1 ..< 6, bits: 1...), 30 | .progressive((Cr, \.0), band: 1 ..< 6, bits: 1...), 31 | 32 | .progressive((Cb, \.0), band: 6 ..< 64, bits: 1...), 33 | .progressive((Cr, \.0), band: 6 ..< 64, bits: 1...), 34 | 35 | .progressive((Y, \.0), band: 1 ..< 64, bit: 0 ), 36 | .progressive((Cb, \.0), band: 1 ..< 64, bit: 0 ), 37 | .progressive((Cr, \.0), band: 1 ..< 64, bit: 0 ), 38 | ]) 39 | 40 | let quanta:([UInt16], [UInt16]) = 41 | ( 42 | .init(repeating: 1, count: 64), 43 | .init(repeating: 1, count: 64) 44 | ) 45 | 46 | var planar:JPEG.Data.Planar = .init( 47 | size: (8, 8), 48 | layout: layout, 49 | metadata: 50 | [ 51 | .jfif(.init(version: .v1_2, density: (1, 1, .centimeters))), 52 | ]) 53 | 54 | let colors:[JPEG.YCbCr] = ((0 as UInt8) ..< (8 * 8 as UInt8)).map 55 | { 56 | let rgb:JPEG.RGB = .init( 57 | UInt8.random(in: 5 ... 250), 58 | 128 + ($0 & 0x38) - 32, 59 | 128 + ($0 << 3 & 0x38) - 32) 60 | return rgb.ycc 61 | } 62 | planar.with(ci: Y) 63 | { 64 | for (x, y):(Int, Int) in $0.indices 65 | { 66 | $0[x: x, y: y] = .init(colors[8 * y + x].y) 67 | } 68 | } 69 | planar.with(ci: Cb) 70 | { 71 | for (x, y):(Int, Int) in $0.indices 72 | { 73 | $0[x: x, y: y] = .init(colors[8 * y + x].cb) 74 | } 75 | } 76 | planar.with(ci: Cr) 77 | { 78 | for (x, y):(Int, Int) in $0.indices 79 | { 80 | $0[x: x, y: y] = .init(colors[8 * y + x].cr) 81 | } 82 | } 83 | 84 | let spectral:JPEG.Data.Spectral = planar.fdct(quanta: 85 | [ 86 | 0: quanta.0, 87 | 1: quanta.1, 88 | ]) 89 | 90 | guard let _:Void = try spectral.compress(path: path) 91 | else 92 | { 93 | fatalError("failed to open file '\(path)'") 94 | } 95 | } 96 | 97 | func print(histogram:[Int], width:Int) 98 | { 99 | // print histogram 100 | let max:Int = histogram.max() ?? 1 101 | for (y, count):(Int, Int) in zip(histogram.indices, histogram) 102 | { 103 | let value:UInt8 = .init(y), 104 | rgb:(UInt8, UInt8, UInt8) = (value, value, value) 105 | let left:String = Highlight.highlight(" y = \(String.pad("\(y)", left: 3)) ", rgb) 106 | let label:String = .pad("\(count)", left: 5) 107 | let right:String = .init(repeating: "█", count: width * count / max) 108 | 109 | print("\(left) \(label) \(right)") 110 | } 111 | } 112 | 113 | func generate(count:Int, prefix:String) throws 114 | { 115 | var rng:SystemRandomNumberGenerator = .init() 116 | var histogram:[Int] = .init(repeating: 0, count: 256) 117 | for i:Int in 0 ..< count 118 | { 119 | let path:String = "\(prefix)/\(i).jpg" 120 | try fuzz(rng: &rng, path: path) 121 | 122 | guard let rectangular:JPEG.Data.Rectangular = 123 | try .decompress(path: path) 124 | else 125 | { 126 | fatalError("failed to open file '\(path)'") 127 | } 128 | 129 | // merge into histogram 130 | let ycc:[JPEG.YCbCr] = rectangular.unpack(as: JPEG.YCbCr.self) 131 | for pixel:JPEG.YCbCr in ycc 132 | { 133 | histogram[.init(pixel.y)] += 1 134 | } 135 | 136 | // terminal output 137 | print(path) 138 | let image:[JPEG.RGB] = rectangular.unpack(as: JPEG.RGB.self) 139 | for i:Int in 0 ..< rectangular.size.y 140 | { 141 | let line:String = (0 ..< rectangular.size.x).map 142 | { 143 | (j:Int) in 144 | 145 | let c:JPEG.RGB = image[j + i * rectangular.size.x] 146 | return Highlight.square((c.r, c.g, c.b)) 147 | }.joined(separator: "") 148 | print(line) 149 | } 150 | } 151 | print(histogram: histogram, width: 80) 152 | } 153 | 154 | enum Parameter:String 155 | { 156 | case count 157 | case path 158 | } 159 | 160 | var parameter:Parameter? = nil 161 | var count:Int = 16 162 | var prefix:String = "tests/fuzz/data/jpeg" 163 | for argument:String in CommandLine.arguments.dropFirst() 164 | { 165 | if argument.prefix(2) == "--" 166 | { 167 | guard let p:Parameter = Parameter.init(rawValue: .init(argument.dropFirst(2))) 168 | else 169 | { 170 | fatalError("unrecognized parameter '\(argument)'") 171 | } 172 | 173 | parameter = p 174 | } 175 | else 176 | { 177 | switch parameter 178 | { 179 | case nil: 180 | fatalError("no parameter name given before argument value '\(argument)'") 181 | 182 | case .count?: 183 | guard let n:Int = Int.init(argument) 184 | else 185 | { 186 | fatalError("could not convert argument '\(argument)' to Int") 187 | } 188 | count = n 189 | 190 | case .path: 191 | prefix = argument 192 | } 193 | } 194 | } 195 | 196 | try generate(count: count, prefix: prefix) 197 | -------------------------------------------------------------------------------- /Snippets/Rotate.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | enum Parameter:String 5 | { 6 | case rotation 7 | } 8 | enum Rotation:String 9 | { 10 | case ii, iii, iv 11 | } 12 | 13 | enum Block 14 | { 15 | typealias Coefficient = (z:Int, multiplier:Int16) 16 | 17 | static 18 | func transpose(_ input:[Coefficient]) -> [Coefficient] 19 | { 20 | (0 ..< 8).flatMap 21 | { 22 | (y:Int) in 23 | (0 ..< 8).map 24 | { 25 | (x:Int) in 26 | input[8 * x + y] 27 | } 28 | } 29 | } 30 | static 31 | func reflectVertical(_ input:[Coefficient]) -> [Coefficient] 32 | { 33 | (0 ..< 8).flatMap 34 | { 35 | (y:Int) -> [Coefficient] in 36 | (0 ..< 8).map 37 | { 38 | (x:Int) -> Coefficient in 39 | ( 40 | input[8 * y + x].z, 41 | input[8 * y + x].multiplier * (1 - 2 * (.init(y) & 1)) 42 | ) 43 | } 44 | } 45 | } 46 | static 47 | func reflectHorizontal(_ input:[Coefficient]) -> [Coefficient] 48 | { 49 | (0 ..< 8).flatMap 50 | { 51 | (y:Int) -> [Coefficient] in 52 | (0 ..< 8).map 53 | { 54 | (x:Int) -> Coefficient in 55 | ( 56 | input[8 * y + x].z, 57 | input[8 * y + x].multiplier * (1 - 2 * (.init(x) & 1)) 58 | ) 59 | } 60 | } 61 | } 62 | 63 | static 64 | func transform(_ body:([Coefficient]) -> [Coefficient]) -> [Coefficient] 65 | { 66 | let blank:[Coefficient] = (0 ..< 8).flatMap 67 | { 68 | (y:Int) -> [Coefficient] in 69 | (0 ..< 8).map 70 | { 71 | (x:Int) in 72 | (JPEG.Table.Quantization.z(k: x, h: y), 1) 73 | } 74 | } 75 | let result:[Coefficient] = body(blank) 76 | let zigzag:[Coefficient] = .init(unsafeUninitializedCapacity: 64) 77 | { 78 | for h:Int in 0 ..< 8 79 | { 80 | for k:Int in 0 ..< 8 81 | { 82 | let z:Int = JPEG.Table.Quantization.z(k: k, h: h) 83 | $0[z] = result[8 * h + k] 84 | } 85 | } 86 | $1 = 64 87 | } 88 | return zigzag 89 | } 90 | } 91 | 92 | func rotate(_ rotation:Rotation, input:String, output:String) throws 93 | { 94 | guard var original:JPEG.Data.Spectral = try .decompress(path: input) 95 | else 96 | { 97 | fatalError("failed to open file '\(input)'") 98 | } 99 | 100 | let scale:(x:Int, y:Int) = original.layout.scale 101 | 102 | let mapping:[Block.Coefficient] 103 | let matrix:(x:(x:Int, y:Int), y:(x:Int, y:Int)) 104 | let size:(x:Int, y:Int) 105 | switch rotation 106 | { 107 | case .ii: 108 | original.set(width: original.size.x - original.size.x % (8 * scale.x)) 109 | size = (original.size.y, original.size.x) 110 | mapping = Block.transform 111 | { 112 | Block.reflectVertical(Block.transpose($0)) 113 | } 114 | matrix = 115 | ( 116 | ( 0, 1), 117 | (-1, 0) 118 | ) 119 | 120 | case .iii: 121 | original.set(width: original.size.x - original.size.x % (8 * scale.x)) 122 | original.set(height: original.size.y - original.size.y % (8 * scale.y)) 123 | size = original.size 124 | mapping = Block.transform 125 | { 126 | Block.reflectVertical(Block.reflectHorizontal($0)) 127 | } 128 | matrix = 129 | ( 130 | (-1, 0), 131 | ( 0, -1) 132 | ) 133 | case .iv: 134 | original.set(height: original.size.y - original.size.y % (8 * scale.y)) 135 | size = (original.size.y, original.size.x) 136 | mapping = Block.transform 137 | { 138 | Block.reflectHorizontal(Block.transpose($0)) 139 | } 140 | matrix = 141 | ( 142 | ( 0, -1), 143 | ( 1, 0) 144 | ) 145 | } 146 | 147 | var rotated:JPEG.Data.Spectral = .init( 148 | size: size, 149 | layout: original.layout, 150 | metadata: original.metadata, 151 | quanta: original.quanta.mapValues 152 | { 153 | (old:[UInt16]) in 154 | .init(unsafeUninitializedCapacity: 64) 155 | { 156 | for z:Int in 0 ..< 64 157 | { 158 | $0[z] = old[mapping[z].z] 159 | } 160 | 161 | $1 = 64 162 | } 163 | }) 164 | 165 | // loop through planes 166 | for p:(Int, Int) in zip(original.indices, rotated.indices) 167 | { 168 | let period:(x:Int, y:Int) = original[p.0].units 169 | let offset:(x:Int, y:Int) = 170 | ( 171 | (matrix.x.x < 0 ? period.x - 1 : 0) + (matrix.x.y < 0 ? period.y - 1 : 0), 172 | (matrix.y.x < 0 ? period.x - 1 : 0) + (matrix.y.y < 0 ? period.y - 1 : 0) 173 | ) 174 | // loop through blocks 175 | for s:(x:Int, y:Int) in original[p.0].indices 176 | { 177 | let d:(x:Int, y:Int) = 178 | ( 179 | offset.x + matrix.x.x * s.x + matrix.x.y * s.y, 180 | offset.y + matrix.y.x * s.x + matrix.y.y * s.y 181 | ) 182 | 183 | // loop through coefficients 184 | for z:Int in 0 ..< 64 185 | { 186 | rotated[p.1][x: d.x, y: d.y, z: z] = 187 | original[p.0][x: s.x, y: s.y, z: mapping[z].z] * mapping[z].multiplier 188 | } 189 | } 190 | } 191 | 192 | guard let _:Void = try rotated.compress(path: output) 193 | else 194 | { 195 | fatalError("failed to open file '\(output)'") 196 | } 197 | } 198 | 199 | var parameter:Parameter? = nil 200 | var rotation:Rotation = .ii 201 | var input:String? = nil, 202 | output:String? = nil 203 | for argument:String in CommandLine.arguments.dropFirst() 204 | { 205 | if argument.prefix(2) == "--" 206 | { 207 | guard let p:Parameter = Parameter.init(rawValue: .init(argument.dropFirst(2))) 208 | else 209 | { 210 | fatalError("unrecognized parameter '\(argument)'") 211 | } 212 | 213 | parameter = p 214 | } 215 | else 216 | { 217 | switch parameter 218 | { 219 | case nil: 220 | if input == nil 221 | { 222 | input = argument 223 | } 224 | else 225 | { 226 | output = argument 227 | } 228 | 229 | case .rotation?: 230 | guard let r:Rotation = Rotation.init(rawValue: argument) 231 | else 232 | { 233 | fatalError("'\(argument)' is not a valid rotation specifier (must be 'ii', 'iii', or 'iv')") 234 | } 235 | 236 | rotation = r 237 | } 238 | } 239 | } 240 | 241 | if let input:String = input, 242 | let output:String = output 243 | { 244 | try rotate(rotation, input: input, output: output) 245 | } 246 | -------------------------------------------------------------------------------- /Snippets/DecodeOnline.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGSystem 3 | 4 | struct Stream 5 | { 6 | private(set) 7 | var data:[UInt8], 8 | position:Int, 9 | available:Int 10 | } 11 | extension Stream:JPEG.Bytestream.Source 12 | { 13 | init(_ data:[UInt8]) 14 | { 15 | self.data = data 16 | self.position = data.startIndex 17 | self.available = data.startIndex 18 | } 19 | 20 | mutating 21 | func read(count:Int) -> [UInt8]? 22 | { 23 | guard self.position + count <= data.endIndex 24 | else 25 | { 26 | return nil 27 | } 28 | guard self.position + count < self.available 29 | else 30 | { 31 | self.available += 4096 32 | return nil 33 | } 34 | 35 | defer 36 | { 37 | self.position += count 38 | } 39 | 40 | return .init(self.data[self.position ..< self.position + count]) 41 | } 42 | 43 | mutating 44 | func reset(position:Int) 45 | { 46 | precondition(self.data.indices ~= position) 47 | self.position = position 48 | } 49 | } 50 | 51 | let path:String = "examples/decode-online/karlie-oscars-2017.jpg" 52 | guard let data:[UInt8] = (System.File.Source.open(path: path) 53 | { 54 | (source:inout System.File.Source) -> [UInt8]? in 55 | 56 | guard let count:Int = source.count 57 | else 58 | { 59 | return nil 60 | } 61 | return source.read(count: count) 62 | } ?? nil) 63 | else 64 | { 65 | fatalError("failed to open or read file '\(path)'") 66 | } 67 | 68 | var stream:Stream = .init(data) 69 | 70 | func waitSegment(stream:inout Stream) throws -> (JPEG.Marker, [UInt8]) 71 | { 72 | let position:Int = stream.position 73 | while true 74 | { 75 | do 76 | { 77 | return try stream.segment() 78 | } 79 | catch JPEG.LexingError.truncatedMarkerSegmentType 80 | { 81 | stream.reset(position: position) 82 | continue 83 | } 84 | catch JPEG.LexingError.truncatedMarkerSegmentHeader 85 | { 86 | stream.reset(position: position) 87 | continue 88 | } 89 | catch JPEG.LexingError.truncatedMarkerSegmentBody 90 | { 91 | stream.reset(position: position) 92 | continue 93 | } 94 | catch JPEG.LexingError.truncatedEntropyCodedSegment 95 | { 96 | stream.reset(position: position) 97 | continue 98 | } 99 | } 100 | } 101 | func waitSegmentPrefix(stream:inout Stream) throws -> ([UInt8], (JPEG.Marker, [UInt8])) 102 | { 103 | let position:Int = stream.position 104 | while true 105 | { 106 | do 107 | { 108 | return try stream.segment(prefix: true) 109 | } 110 | catch JPEG.LexingError.truncatedMarkerSegmentType 111 | { 112 | stream.reset(position: position) 113 | continue 114 | } 115 | catch JPEG.LexingError.truncatedMarkerSegmentHeader 116 | { 117 | stream.reset(position: position) 118 | continue 119 | } 120 | catch JPEG.LexingError.truncatedMarkerSegmentBody 121 | { 122 | stream.reset(position: position) 123 | continue 124 | } 125 | catch JPEG.LexingError.truncatedEntropyCodedSegment 126 | { 127 | stream.reset(position: position) 128 | continue 129 | } 130 | } 131 | } 132 | 133 | func decodeOnline(stream:inout Stream, _ capture:(JPEG.Data.Spectral) throws -> ()) 134 | throws 135 | { 136 | var marker:(type:JPEG.Marker, data:[UInt8]) 137 | 138 | // start of image 139 | marker = try waitSegment(stream: &stream) 140 | guard case .start = marker.type 141 | else 142 | { 143 | fatalError() 144 | } 145 | marker = try waitSegment(stream: &stream) 146 | 147 | var dc:[JPEG.Table.HuffmanDC] = [], 148 | ac:[JPEG.Table.HuffmanAC] = [], 149 | quanta:[JPEG.Table.Quantization] = [] 150 | var interval:JPEG.Header.RestartInterval?, 151 | frame:JPEG.Header.Frame? 152 | definitions: 153 | while true 154 | { 155 | switch marker.type 156 | { 157 | case .frame(let process): 158 | frame = try .parse(marker.data, process: process) 159 | marker = try waitSegment(stream: &stream) 160 | break definitions 161 | 162 | case .quantization: 163 | let parsed:[JPEG.Table.Quantization] = 164 | try JPEG.Table.parse(quantization: marker.data) 165 | quanta.append(contentsOf: parsed) 166 | 167 | case .huffman: 168 | let parsed:(dc:[JPEG.Table.HuffmanDC], ac:[JPEG.Table.HuffmanAC]) = 169 | try JPEG.Table.parse(huffman: marker.data) 170 | dc.append(contentsOf: parsed.dc) 171 | ac.append(contentsOf: parsed.ac) 172 | 173 | case .interval: 174 | interval = try .parse(marker.data) 175 | 176 | // ignore 177 | case .application, .comment: 178 | break 179 | 180 | // unexpected 181 | case .scan, .height, .end, .start, .restart: 182 | fatalError() 183 | 184 | // unsupported 185 | case .arithmeticCodingCondition, .hierarchical, .expandReferenceComponents: 186 | break 187 | } 188 | 189 | marker = try waitSegment(stream: &stream) 190 | } 191 | 192 | // can use `!` here, previous loop cannot exit without initializing `frame` 193 | var context:JPEG.Context = try .init(frame: frame!) 194 | for table:JPEG.Table.HuffmanDC in dc 195 | { 196 | context.push(dc: table) 197 | } 198 | for table:JPEG.Table.HuffmanAC in ac 199 | { 200 | context.push(ac: table) 201 | } 202 | for table:JPEG.Table.Quantization in quanta 203 | { 204 | try context.push(quanta: table) 205 | } 206 | if let interval:JPEG.Header.RestartInterval = interval 207 | { 208 | context.push(interval: interval) 209 | } 210 | 211 | var first:Bool = true 212 | scans: 213 | while true 214 | { 215 | switch marker.type 216 | { 217 | // ignore 218 | case .application, .comment: 219 | break 220 | // unexpected 221 | case .frame, .start, .restart, .height: 222 | fatalError() 223 | // unsupported 224 | case .arithmeticCodingCondition, .hierarchical, .expandReferenceComponents: 225 | break 226 | 227 | case .quantization: 228 | for table:JPEG.Table.Quantization in 229 | try JPEG.Table.parse(quantization: marker.data) 230 | { 231 | try context.push(quanta: table) 232 | } 233 | 234 | case .huffman: 235 | let parsed:(dc:[JPEG.Table.HuffmanDC], ac:[JPEG.Table.HuffmanAC]) = 236 | try JPEG.Table.parse(huffman: marker.data) 237 | for table:JPEG.Table.HuffmanDC in parsed.dc 238 | { 239 | context.push(dc: table) 240 | } 241 | for table:JPEG.Table.HuffmanAC in parsed.ac 242 | { 243 | context.push(ac: table) 244 | } 245 | 246 | case .interval: 247 | context.push(interval: try .parse(marker.data)) 248 | 249 | case .scan: 250 | let scan:JPEG.Header.Scan = try .parse(marker.data, 251 | process: context.spectral.layout.process) 252 | var ecss:[[UInt8]] = [] 253 | for index:Int in 0... 254 | { 255 | let ecs:[UInt8] 256 | (ecs, marker) = try waitSegmentPrefix(stream: &stream) 257 | ecss.append(ecs) 258 | guard case .restart(let phase) = marker.type 259 | else 260 | { 261 | try context.push(scan: scan, ecss: ecss, extend: first) 262 | if first 263 | { 264 | let height:JPEG.Header.HeightRedefinition 265 | if case .height = marker.type 266 | { 267 | height = try .parse(marker.data) 268 | marker = try waitSegment(stream: &stream) 269 | } 270 | // same guarantees for `!` as before 271 | else if frame!.size.y > 0 272 | { 273 | height = .init(height: frame!.size.y) 274 | } 275 | else 276 | { 277 | fatalError() 278 | } 279 | context.push(height: height) 280 | first = false 281 | } 282 | 283 | print("band: \(scan.band), bits: \(scan.bits), components: \(scan.components.map(\.ci))") 284 | try capture(context.spectral) 285 | continue scans 286 | } 287 | 288 | guard phase == index % 8 289 | else 290 | { 291 | fatalError() 292 | } 293 | } 294 | 295 | case .end: 296 | return 297 | } 298 | 299 | marker = try waitSegment(stream: &stream) 300 | } 301 | } 302 | 303 | var counter:Int = 0 304 | var last:[JPEG.RGB] = [] 305 | try decodeOnline(stream: &stream) 306 | { 307 | let image:JPEG.Data.Rectangular = $0.idct().interleaved() 308 | let rgb:[JPEG.RGB] = image.unpack(as: JPEG.RGB.self) 309 | let difference:[JPEG.RGB] = 310 | zip(last + .init(repeating: .init(0, 0, 0), count: rgb.count - last.count), rgb).map 311 | { 312 | let d:(r:Int, g:Int, b:Int) = 313 | ( 314 | abs(.init($1.r) - .init($0.r)), 315 | abs(.init($1.g) - .init($0.g)), 316 | abs(.init($1.b) - .init($0.b)) 317 | ) 318 | return .init(.init(clamping: 50 * d.r), .init(clamping: 50 * d.g), .init(clamping: 50 * d.b)) 319 | } 320 | 321 | last = rgb 322 | 323 | guard let _:Void = (System.File.Destination.open(path: "\(path)-\(counter).rgb") 324 | { 325 | guard let _:Void = $0.write(rgb.flatMap{ [$0.r, $0.g, $0.b] }) 326 | else 327 | { 328 | fatalError("failed to write to file '\(path)-\(counter).rgb'") 329 | } 330 | }) 331 | else 332 | { 333 | fatalError("failed to open file '\(path)-\(counter).rgb'") 334 | } 335 | 336 | guard let _:Void = (System.File.Destination.open(path: "\(path)-difference-\(counter).rgb") 337 | { 338 | guard let _:Void = $0.write(difference.flatMap{ [$0.r, $0.g, $0.b] }) 339 | else 340 | { 341 | fatalError("failed to write to file '\(path)-difference-\(counter).rgb'") 342 | } 343 | }) 344 | else 345 | { 346 | fatalError("failed to open file '\(path)-difference-\(counter).rgb'") 347 | } 348 | 349 | counter += 1 350 | } 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 191 | Dianna Ma (@taylorswift, @tayloraswift) 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /Sources/JPEG/common.swift: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | /// A namespace for general functionality. 6 | public 7 | enum General 8 | { 9 | } 10 | 11 | extension General 12 | { 13 | /// A property wrapper providing an immutable ``Int`` interface backed 14 | /// by a different integer type. 15 | @propertyWrapper 16 | public 17 | struct Storage where I:FixedWidthInteger & BinaryInteger 18 | { 19 | private 20 | var storage:I 21 | /// Creates an instance of this property wrapper, with the given value 22 | /// truncated to the width of the storage type `I`. 23 | /// 24 | /// - Parameter wrappedValue: 25 | /// The value to wrap. 26 | public 27 | init(wrappedValue:Int) 28 | { 29 | self.storage = .init(truncatingIfNeeded: wrappedValue) 30 | } 31 | /// The value wrapped by this property wrapper, expanded to an ``Int``. 32 | public 33 | var wrappedValue:Int 34 | { 35 | .init(self.storage) 36 | } 37 | } 38 | /// A property wrapper providing an immutable `(```Int```, ```Int```)` 39 | /// interface backed by a different integer type. 40 | @propertyWrapper 41 | public 42 | struct Storage2 where I:FixedWidthInteger & BinaryInteger 43 | { 44 | private 45 | var storage:(x:I, y:I) 46 | /// Creates an instance of this property wrapper, with the given values 47 | /// truncated to the width of the storage type `I`. 48 | /// 49 | /// - Parameter wrappedValue: 50 | /// The values to wrap. 51 | public 52 | init(wrappedValue:(x:Int, y:Int)) 53 | { 54 | self.storage = 55 | ( 56 | .init(truncatingIfNeeded: wrappedValue.x), 57 | .init(truncatingIfNeeded: wrappedValue.y) 58 | ) 59 | } 60 | /// The values wrapped by this property wrapper, expanded to an 61 | /// `(```Int```, ```Int```)` tuple. 62 | public 63 | var wrappedValue:(x:Int, y:Int) 64 | { 65 | (.init(self.storage.x), .init(self.storage.y)) 66 | } 67 | } 68 | /// A property wrapper providing a mutable ``Int`` interface backed 69 | /// by a different integer type. 70 | @propertyWrapper 71 | public 72 | struct MutableStorage where I:FixedWidthInteger & BinaryInteger 73 | { 74 | private 75 | var storage:I 76 | /// Creates an instance of this property wrapper, with the given value 77 | /// truncated to the width of the storage type `I`. 78 | /// 79 | /// - Parameter wrappedValue: 80 | /// The value to wrap. 81 | public 82 | init(wrappedValue:Int) 83 | { 84 | self.storage = .init(truncatingIfNeeded: wrappedValue) 85 | } 86 | /// The value wrapped by this property wrapper, expanded to an ``Int``. 87 | public 88 | var wrappedValue:Int 89 | { 90 | get 91 | { 92 | .init(self.storage) 93 | } 94 | set(value) 95 | { 96 | self.storage = .init(value) 97 | } 98 | } 99 | } 100 | } 101 | 102 | extension General 103 | { 104 | struct Heap where Key:Comparable 105 | { 106 | private 107 | var storage:[(Key, Value)] 108 | 109 | // support 1-based indexing 110 | private 111 | subscript(index:Int) -> (key:Key, value:Value) 112 | { 113 | get 114 | { 115 | self.storage[index - 1] 116 | } 117 | set(item) 118 | { 119 | self.storage[index - 1] = item 120 | } 121 | } 122 | 123 | var count:Int 124 | { 125 | self.storage.count 126 | } 127 | var first:(key:Key, value:Value)? 128 | { 129 | self.storage.first 130 | } 131 | var isEmpty:Bool 132 | { 133 | self.storage.isEmpty 134 | } 135 | 136 | private 137 | var startIndex:Int 138 | { 139 | 1 140 | } 141 | private 142 | var endIndex:Int 143 | { 144 | 1 + self.count 145 | } 146 | } 147 | } 148 | extension General.Heap 149 | { 150 | @inline(__always) 151 | private static 152 | func left(index:Int) -> Int 153 | { 154 | return index << 1 155 | } 156 | @inline(__always) 157 | private static 158 | func right(index:Int) -> Int 159 | { 160 | return index << 1 + 1 161 | } 162 | @inline(__always) 163 | private static 164 | func parent(index:Int) -> Int 165 | { 166 | return index >> 1 167 | } 168 | 169 | private 170 | func highest(above child:Int) -> Int? 171 | { 172 | let p:Int = Self.parent(index: child) 173 | // make sure it’s not the root 174 | guard p >= self.startIndex 175 | else 176 | { 177 | return nil 178 | } 179 | 180 | // and the element is higher than the parent 181 | return self[child].key < self[p].key ? p : nil 182 | } 183 | private 184 | func lowest(below parent:Int) -> Int? 185 | { 186 | let r:Int = Self.right(index: parent), 187 | l:Int = Self.left (index: parent) 188 | 189 | guard l < self.endIndex 190 | else 191 | { 192 | return nil 193 | } 194 | 195 | guard r < self.endIndex 196 | else 197 | { 198 | return self[l].key < self[parent].key ? l : nil 199 | } 200 | 201 | let c:Int = self[r].key < self[l].key ? r : l 202 | return self[c].key < self[parent].key ? c : nil 203 | } 204 | 205 | 206 | @inline(__always) 207 | private mutating 208 | func swapAt(_ i:Int, _ j:Int) 209 | { 210 | self.storage.swapAt(i - 1, j - 1) 211 | } 212 | private mutating 213 | func siftUp(index:Int) 214 | { 215 | guard let parent:Int = self.highest(above: index) 216 | else 217 | { 218 | return 219 | } 220 | 221 | self.swapAt(index, parent) 222 | self.siftUp(index: parent) 223 | } 224 | private mutating 225 | func siftDown(index:Int) 226 | { 227 | guard let child:Int = self.lowest(below: index) 228 | else 229 | { 230 | return 231 | } 232 | 233 | self.swapAt (index, child) 234 | self.siftDown(index: child) 235 | } 236 | 237 | mutating 238 | func enqueue(key:Key, value:Value) 239 | { 240 | self.storage.append((key, value)) 241 | self.siftUp(index: self.endIndex - 1) 242 | } 243 | 244 | mutating 245 | func dequeue() -> (key:Key, value:Value)? 246 | { 247 | switch self.count 248 | { 249 | case 0: 250 | return nil 251 | case 1: 252 | return self.storage.removeLast() 253 | default: 254 | self.swapAt(self.startIndex, self.endIndex - 1) 255 | defer 256 | { 257 | self.siftDown(index: self.startIndex) 258 | } 259 | return self.storage.removeLast() 260 | } 261 | } 262 | 263 | init(_ sequence:S) where S:Sequence, S.Element == (Key, Value) 264 | { 265 | self.storage = .init(sequence) 266 | // heapify 267 | let halfway:Int = Self.parent(index: self.endIndex - 1) + 1 268 | for i:Int in (self.startIndex ..< halfway).reversed() 269 | { 270 | self.siftDown(index: i) 271 | } 272 | } 273 | } 274 | extension General.Heap:ExpressibleByArrayLiteral 275 | { 276 | init(arrayLiteral:(key:Key, value:Value)...) 277 | { 278 | self.init(arrayLiteral) 279 | } 280 | } 281 | 282 | // 2d iterators 283 | extension General 284 | { 285 | /// A two-dimensional open range. 286 | public 287 | struct Range2 where Bound:Comparable 288 | { 289 | let lowerBound:(x:Bound, y:Bound) 290 | let upperBound:(x:Bound, y:Bound) 291 | 292 | init(lowerBound:(x:Bound, y:Bound), upperBound:(x:Bound, y:Bound)) 293 | { 294 | precondition(lowerBound.x <= upperBound.x, "x lower bound cannot be greater than upper bound") 295 | precondition(lowerBound.y <= upperBound.y, "y lower bound cannot be greater than upper bound") 296 | 297 | self.lowerBound = lowerBound 298 | self.upperBound = upperBound 299 | } 300 | } 301 | 302 | } 303 | func ..< (lhs:(x:Bound, y:Bound), rhs:(x:Bound, y:Bound)) -> General.Range2 304 | where Bound:Comparable 305 | { 306 | return .init(lowerBound: lhs, upperBound: rhs) 307 | } 308 | 309 | extension General.Range2:Sequence where Bound:Strideable, Bound.Stride:SignedInteger 310 | { 311 | public 312 | typealias Element = (x:Bound, y:Bound) 313 | 314 | /// A two-dimensional range iterator. 315 | public 316 | struct Iterator 317 | { 318 | var x:Bound, 319 | y:Bound 320 | let bound:(x:(Bound, Bound), y:Bound) 321 | } 322 | 323 | /// Creates an iterator for this range instance. 324 | /// 325 | /// This iterator will traverse the range space in row-major order. For 326 | /// example, if the bounds are `(x: 0, y: 0)` and `(x: 2, y: 2)`, the iterator 327 | /// will yield the elements `(x: 0, y: 0)`, `(x: 1, y: 0)`, `(x: 0, y: 1)`, 328 | /// and `(x: 1, y: 1)`, in that order. 329 | public 330 | func makeIterator() -> Iterator 331 | { 332 | .init(x: self.lowerBound.x, y: self.lowerBound.y, 333 | bound: ((self.lowerBound.x, self.upperBound.x), self.upperBound.y)) 334 | } 335 | } 336 | extension General.Range2.Iterator:IteratorProtocol 337 | { 338 | /// Advances to the next element and returns it, or `nil` if no next element exists. 339 | /// 340 | /// - Returns: 341 | /// The next element in the two-dimensional range sequence, if it exists, 342 | /// otherwise `nil`. If advancing the `x` index would cause it to reach its 343 | /// upper bound, this iterator will advance to the next `y` index and reset 344 | /// the `x` index to its lower bound. 345 | public mutating 346 | func next() -> (x:Bound, y:Bound)? 347 | { 348 | if self.x < self.bound.x.1 349 | { 350 | defer 351 | { 352 | self.x = self.x.advanced(by: 1) 353 | } 354 | 355 | return (self.x, self.y) 356 | } 357 | else 358 | { 359 | self.y = self.y.advanced(by: 1) 360 | 361 | if self.y < self.bound.y 362 | { 363 | self.x = self.bound.x.0 364 | return self.next() 365 | } 366 | else 367 | { 368 | return nil 369 | } 370 | } 371 | } 372 | } 373 | 374 | // raw buffer utilities 375 | extension ArraySlice where Element == UInt8 376 | { 377 | /// Loads this array slice as a misaligned big-endian integer value, 378 | /// and casts it to a desired format. 379 | /// - Parameters: 380 | /// - bigEndian: The size and type to interpret this array slice as. 381 | /// - type: The type to cast the read integer value to. 382 | /// - Returns: The read integer value, cast to `U`. 383 | func load(bigEndian:T.Type, as type:U.Type) -> U 384 | where T:FixedWidthInteger, U:BinaryInteger 385 | { 386 | return self.withUnsafeBufferPointer 387 | { 388 | (buffer:UnsafeBufferPointer) in 389 | 390 | assert(buffer.count >= MemoryLayout.size, 391 | "attempt to load \(T.self) from slice of size \(buffer.count)") 392 | 393 | var storage:T = .init() 394 | let value:T = withUnsafeMutablePointer(to: &storage) 395 | { 396 | $0.deinitialize(count: 1) 397 | 398 | let source:UnsafeRawPointer = .init(buffer.baseAddress!), 399 | raw:UnsafeMutableRawPointer = .init($0) 400 | 401 | raw.copyMemory(from: source, byteCount: MemoryLayout.size) 402 | 403 | return raw.load(as: T.self) 404 | } 405 | 406 | return U(T(bigEndian: value)) 407 | } 408 | } 409 | } 410 | extension Array where Element == UInt8 411 | { 412 | /// Loads a misaligned big-endian integer value from the given byte offset 413 | /// and casts it to a desired format. 414 | /// - Parameters: 415 | /// - bigEndian: The size and type to interpret the data to load as. 416 | /// - type: The type to cast the read integer value to. 417 | /// - byte: The byte offset to load the big-endian integer from. 418 | /// - Returns: The read integer value, cast to `U`. 419 | func load(bigEndian:T.Type, as type:U.Type, at byte:Int) -> U 420 | where T:FixedWidthInteger, U:BinaryInteger 421 | { 422 | return self[byte ..< byte + MemoryLayout.size].load(bigEndian: T.self, as: U.self) 423 | } 424 | } 425 | 426 | extension Array where Element == UInt8 427 | { 428 | /// Decomposes the given integer value into its constituent bytes, in big-endian order. 429 | /// - Parameters: 430 | /// - value: The integer value to decompose. 431 | /// - type: The big-endian format `T` to store the given `value` as. The given 432 | /// `value` is truncated to fit in a `T`. 433 | /// - Returns: An array containing the bytes of the given `value`, in big-endian order. 434 | static 435 | func store(_ value:U, asBigEndian type:T.Type) -> [UInt8] 436 | where U:BinaryInteger, T:FixedWidthInteger 437 | { 438 | return .init(unsafeUninitializedCapacity: MemoryLayout.size) 439 | { 440 | (buffer:inout UnsafeMutableBufferPointer, count:inout Int) in 441 | 442 | let bigEndian:T = T.init(truncatingIfNeeded: value).bigEndian, 443 | destination:UnsafeMutableRawBufferPointer = .init(buffer) 444 | Swift.withUnsafeBytes(of: bigEndian) 445 | { 446 | destination.copyMemory(from: $0) 447 | count = $0.count 448 | } 449 | } 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /Sources/JPEGSystem/os.swift: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 | 5 | import JPEG 6 | 7 | #if os(macOS) 8 | import Darwin 9 | #elseif os(Linux) 10 | import Glibc 11 | #elseif os(Android) 12 | import Android 13 | #else 14 | #warning("unsupported or untested platform (please open an issue at https://github.com/tayloraswift/swift-jpeg/issues)") 15 | #endif 16 | 17 | #if os(macOS) || os(Linux) || os(Android) 18 | 19 | /// A namespace for platform-dependent functionality. 20 | /// 21 | /// These APIs are only available on MacOS and Linux. However, the rest of the 22 | /// framework is pure Swift and should support all Swift platforms. 23 | public 24 | enum System 25 | { 26 | /// A namespace for file IO functionality. 27 | public 28 | enum File 29 | { 30 | #if os(Android) 31 | typealias Descriptor = OpaquePointer 32 | #else 33 | typealias Descriptor = UnsafeMutablePointer 34 | #endif 35 | 36 | /// A type for reading data from files on disk. 37 | public 38 | struct Source 39 | { 40 | private 41 | let descriptor:Descriptor 42 | } 43 | 44 | /// A type for writing data to files on disk. 45 | public 46 | struct Destination 47 | { 48 | private 49 | let descriptor:Descriptor 50 | } 51 | } 52 | } 53 | extension System.File.Source 54 | { 55 | /// Calls a closure with an interface for reading from the specified file. 56 | /// 57 | /// This method automatically closes the file when its closure argument returns. 58 | /// 59 | /// - Parameter path: 60 | /// The path to the file to open. 61 | /// 62 | /// - Parameter body: 63 | /// A closure with a ``Source`` parameter from which data in 64 | /// the specified file can be read. This interface is only valid 65 | /// for the duration of the method’s execution. The closure is 66 | /// only executed if the specified file could be successfully 67 | /// opened, otherwise this method will return `nil`. If `body` has a 68 | /// return value and the specified file could be opened, this method 69 | /// returns the return value of the closure. 70 | /// 71 | /// - Returns: 72 | /// The return value of the closure argument, or `nil` if the specified 73 | /// file could not be opened. 74 | public static 75 | func open(path:String, _ body:(inout Self) throws -> R) 76 | rethrows -> R? 77 | { 78 | guard let descriptor:System.File.Descriptor = fopen(path, "rb") 79 | else 80 | { 81 | return nil 82 | } 83 | 84 | var file:Self = .init(descriptor: descriptor) 85 | defer 86 | { 87 | fclose(file.descriptor) 88 | } 89 | 90 | return try body(&file) 91 | } 92 | 93 | /// Reads the specified number of bytes from this file interface. 94 | /// 95 | /// This method only returns an array if the exact number of bytes 96 | /// specified could be read. This method advances the file pointer. 97 | /// 98 | /// - Parameter capacity: 99 | /// The number of bytes to read. 100 | /// 101 | /// - Returns: 102 | /// An array containing the read data, or `nil` if the specified 103 | /// number of bytes could not be read. 104 | public 105 | func read(count capacity:Int) -> [UInt8]? 106 | { 107 | let buffer:[UInt8] = .init(unsafeUninitializedCapacity: capacity) 108 | { 109 | (buffer:inout UnsafeMutableBufferPointer, count:inout Int) in 110 | 111 | #if os(Android) 112 | let baseAddress = buffer.baseAddress! 113 | #else 114 | let baseAddress = buffer.baseAddress 115 | #endif 116 | count = fread(baseAddress, MemoryLayout.stride, 117 | capacity, self.descriptor) 118 | } 119 | 120 | guard buffer.count == capacity 121 | else 122 | { 123 | return nil 124 | } 125 | 126 | return buffer 127 | } 128 | /// The size of the file, in bytes, or `nil` if the file is not a regular 129 | /// file or a link to a file. 130 | /// 131 | /// This property queries the file size using `stat`. 132 | public 133 | var count:Int? 134 | { 135 | let descriptor:Int32 = fileno(self.descriptor) 136 | guard descriptor != -1 137 | else 138 | { 139 | return nil 140 | } 141 | 142 | guard let status:stat = 143 | ({ 144 | var status:stat = .init() 145 | guard fstat(descriptor, &status) == 0 146 | else 147 | { 148 | return nil 149 | } 150 | return status 151 | }()) 152 | else 153 | { 154 | return nil 155 | } 156 | 157 | switch status.st_mode & S_IFMT 158 | { 159 | case S_IFREG, S_IFLNK: 160 | break 161 | default: 162 | return nil 163 | } 164 | 165 | return Int.init(status.st_size) 166 | } 167 | } 168 | extension System.File.Destination 169 | { 170 | /// Calls a closure with an interface for writing to the specified file. 171 | /// 172 | /// This method automatically closes the file when its closure argument returns. 173 | /// 174 | /// - Parameter path: 175 | /// The path to the file to open. 176 | /// 177 | /// - Parameter body: 178 | /// A closure with a ``Destination`` parameter representing 179 | /// the specified file to which data can be written to. This 180 | /// interface is only valid for the duration of the method’s 181 | /// execution. The closure is only executed if the specified file could 182 | /// be successfully opened, otherwise this method will return `nil`. 183 | /// If `body` has a return value and the specified file could be opened, 184 | /// this method returns the return value of the closure. 185 | /// 186 | /// - Returns: 187 | /// The return value of the closure argument, or `nil` if the specified 188 | /// file could not be opened. 189 | public static 190 | func open(path:String, _ body:(inout Self) throws -> R) 191 | rethrows -> R? 192 | { 193 | guard let descriptor:System.File.Descriptor = fopen(path, "wb") 194 | else 195 | { 196 | return nil 197 | } 198 | 199 | var file:Self = .init(descriptor: descriptor) 200 | defer 201 | { 202 | fclose(file.descriptor) 203 | } 204 | 205 | return try body(&file) 206 | } 207 | 208 | /// Write the bytes in the given array to this file interface. 209 | /// 210 | /// This method only returns `()` if the entire array argument could 211 | /// be written. This method advances the file pointer. 212 | /// 213 | /// - Parameter buffer: 214 | /// The data to write. 215 | /// 216 | /// - Returns: 217 | /// A ``Void`` tuple if the entire array argument could be written, 218 | /// or `nil` otherwise. 219 | public 220 | func write(_ buffer:[UInt8]) -> Void? 221 | { 222 | let count:Int = buffer.withUnsafeBufferPointer 223 | { 224 | #if os(Android) 225 | let baseAddress = $0.baseAddress! 226 | #else 227 | let baseAddress = $0.baseAddress 228 | #endif 229 | return fwrite(baseAddress, MemoryLayout.stride, 230 | $0.count, self.descriptor) 231 | } 232 | 233 | guard count == buffer.count 234 | else 235 | { 236 | return nil 237 | } 238 | 239 | return () 240 | } 241 | } 242 | 243 | // declare conformance (as a formality) 244 | extension System.File.Source:JPEG.Bytestream.Source 245 | { 246 | } 247 | extension System.File.Destination:JPEG.Bytestream.Destination 248 | { 249 | } 250 | // file-based encoding and decoding apis 251 | extension JPEG.Data.Spectral 252 | { 253 | /// Decompresses a spectral image from the given file path. 254 | /// 255 | /// Calling this function is equivalent to calling ``System.File.Source.open(path:_:)`` 256 | /// with the closure parameter set to ``decompress(stream:)``. 257 | /// 258 | /// This function is only available on MacOS and Linux platforms. 259 | /// 260 | /// - Parameter path: 261 | /// A file path. 262 | /// 263 | /// - Returns: 264 | /// The decompressed image, or `nil` if the file could not be opened at 265 | /// the given file path. 266 | public static 267 | func decompress(path:String) throws -> Self? 268 | { 269 | return try System.File.Source.open(path: path, Self.decompress(stream:)) 270 | } 271 | /// Compresses a spectral image to the given file path. 272 | /// 273 | /// All metadata records in this image will be emitted at the beginning of 274 | /// the outputted file, in the order they appear in the ``metadata`` array. 275 | /// 276 | /// Calling this function is equivalent to calling ``System.File.Destination.open(path:_:)`` 277 | /// with the closure parameter set to ``compress(stream:)``. 278 | /// 279 | /// This function is only available on MacOS and Linux platforms. 280 | /// 281 | /// - Parameter path: 282 | /// A file path. 283 | /// 284 | /// - Returns: 285 | /// A ``Void`` tuple, or `nil` if the file could not be opened at 286 | /// the given file path. 287 | public 288 | func compress(path:String) throws -> Void? 289 | { 290 | return try System.File.Destination.open(path: path, self.compress(stream:)) 291 | } 292 | } 293 | extension JPEG.Data.Planar 294 | { 295 | /// Decompresses a planar image from the given file path. 296 | /// 297 | /// This function is a convenience function which calls ``Spectral.decompress(path:)`` 298 | /// to obtain a spectral image, and then calls ``Spectral/idct()`` on the 299 | /// output to return a planar image. 300 | /// 301 | /// This function is only available on MacOS and Linux platforms. 302 | /// 303 | /// - Parameter path: 304 | /// A file path. 305 | /// 306 | /// - Returns: 307 | /// The decompressed image, or `nil` if the file could not be opened at 308 | /// the given file path. 309 | public static 310 | func decompress(path:String) throws -> Self? 311 | { 312 | guard let spectral:JPEG.Data.Spectral = try .decompress(path: path) 313 | else 314 | { 315 | return nil 316 | } 317 | return spectral.idct() 318 | } 319 | /// Compresses a planar image to the given file path. 320 | /// 321 | /// All metadata records in this image will be emitted at the beginning of 322 | /// the outputted file, in the order they appear in the ``metadata`` array. 323 | /// 324 | /// This function is a convenience function which calls ``fdct(quanta:)`` 325 | /// to obtain a spectral image, and then calls ``Spectral/compress(path:)`` 326 | /// on the output. 327 | /// 328 | /// This function is only available on MacOS and Linux platforms. 329 | /// 330 | /// - Parameter path: 331 | /// A file path. 332 | /// 333 | /// - Parameter quanta: 334 | /// The quantum values for each quanta key used by this image’s ``layout``, 335 | /// including quanta keys used only by non-recognized components. Each 336 | /// array of quantum values must have exactly 64 elements. The quantization 337 | /// tables created from these values will be encoded using integers with a bit width 338 | /// determined by this image’s `layout.format.precision`, 339 | /// and all the values must be in the correct range for that bit width. 340 | /// 341 | /// - Returns: 342 | /// A ``Void`` tuple, or `nil` if the file could not be opened at 343 | /// the given file path. 344 | public 345 | func compress(path:String, quanta:[JPEG.Table.Quantization.Key: [UInt16]]) throws 346 | -> Void? 347 | { 348 | return try self.fdct(quanta: quanta).compress(path: path) 349 | } 350 | } 351 | extension JPEG.Data.Rectangular 352 | { 353 | /// Decompresses a rectangular image from the given file path. 354 | /// 355 | /// This function is a convenience function which calls ``Planar.decompress(path:)`` 356 | /// to obtain a planar image, and then calls ``Planar/interleaved(cosite:)`` 357 | /// on the output to return a rectangular image. 358 | /// 359 | /// This function is only available on MacOS and Linux platforms. 360 | /// 361 | /// - Parameter path: 362 | /// A file path. 363 | /// 364 | /// - Parameter cosited: 365 | /// The upsampling method to use. Setting this parameter to `true` co-sites 366 | /// the samples; setting it to `false` centers them instead. 367 | /// 368 | /// The default value is `false`. 369 | /// 370 | /// - Returns: 371 | /// The decompressed image, or `nil` if the file could not be opened at 372 | /// the given file path. 373 | public static 374 | func decompress(path:String, cosite cosited:Bool = false) throws -> Self? 375 | { 376 | guard let planar:JPEG.Data.Planar = try .decompress(path: path) 377 | else 378 | { 379 | return nil 380 | } 381 | 382 | return planar.interleaved(cosite: cosited) 383 | } 384 | /// Compresses a rectangular image to the given file path. 385 | /// 386 | /// All metadata records in this image will be emitted at the beginning of 387 | /// the outputted file, in the order they appear in the ``metadata`` array. 388 | /// 389 | /// This function is a convenience function which calls ``decomposed()`` 390 | /// to obtain a planar image, and then calls ``Planar/compress(path:quanta:)`` 391 | /// on the output. 392 | /// 393 | /// This function is only available on MacOS and Linux platforms. 394 | /// 395 | /// - Parameter path: 396 | /// A file path. 397 | /// 398 | /// - Parameter quanta: 399 | /// The quantum values for each quanta key used by this image’s ``layout``, 400 | /// including quanta keys used only by non-recognized components. Each 401 | /// array of quantum values must have exactly 64 elements. The quantization 402 | /// tables created from these values will be encoded using integers with a bit width 403 | /// determined by this image’s `layout.format.precision`, 404 | /// and all the values must be in the correct range for that bit width. 405 | /// 406 | /// - Returns: 407 | /// A ``Void`` tuple, or `nil` if the file could not be opened at 408 | /// the given file path. 409 | public 410 | func compress(path:String, quanta:[JPEG.Table.Quantization.Key: [UInt16]]) throws 411 | -> Void? 412 | { 413 | try self.decomposed().compress(path: path, quanta: quanta) 414 | } 415 | } 416 | 417 | #endif 418 | -------------------------------------------------------------------------------- /tests/integration/tests.swift: -------------------------------------------------------------------------------- 1 | import JPEG 2 | import JPEGInspection 3 | import JPEGSystem 4 | 5 | extension Test 6 | { 7 | static 8 | var cases:[(name:String, function:Function)] 9 | { 10 | [ 11 | ("color-sequential-robustness", .string(Self.decode(_:), 12 | [ 13 | "tests/integration/decode/color-sequential-1.jpg", 14 | "tests/integration/decode/color-sequential-2.jpg", 15 | "tests/integration/decode/color-sequential-3.jpg", 16 | "tests/integration/decode/color-sequential-4.jpg", 17 | ])), 18 | ("grayscale-sequential-robustness", .string(Self.decode(_:), 19 | [ 20 | "tests/integration/decode/grayscale-sequential-1.jpg", 21 | "tests/integration/decode/grayscale-sequential-2.jpg", 22 | ])), 23 | ("color-progressive-robustness", .string(Self.decode(_:), 24 | [ 25 | "tests/integration/decode/color-progressive-1.jpg", 26 | "tests/integration/decode/color-progressive-2.jpg", 27 | "tests/integration/decode/color-progressive-3.jpg", 28 | "tests/integration/decode/color-progressive-4.jpg", 29 | ])), 30 | ("grayscale-progressive-robustness", .string(Self.decode(_:), 31 | [ 32 | "tests/integration/decode/grayscale-progressive-1.jpg", 33 | "tests/integration/decode/grayscale-progressive-2.jpg", 34 | ])), 35 | ("restart-interval-robustness", .string(Self.decode(_:), 36 | [ 37 | "tests/integration/decode/color-sequential-restart.jpg", 38 | "tests/integration/decode/color-progressive-restart.jpg", 39 | "tests/integration/decode/grayscale-sequential-restart.jpg", 40 | "tests/integration/decode/grayscale-progressive-restart.jpg", 41 | ])), 42 | 43 | ("color-sequential-encoding-robustness", .string_int2(Self.encodeColorSequential(_:_:), 44 | [ 45 | ("tests/integration/encode/karlie-kloss-1", (640, 320)) 46 | ])), 47 | ("grayscale-sequential-encoding-robustness", .string_int2(Self.encodeGrayscaleSequential(_:_:), 48 | [ 49 | ("tests/integration/encode/karlie-kloss-1", (640, 320)) 50 | ])), 51 | ("color-progressive-encoding-robustness", .string_int2(Self.encodeColorProgressive(_:_:), 52 | [ 53 | ("tests/integration/encode/karlie-kloss-1", (640, 320)) 54 | ])), 55 | ("grayscale-progressive-encoding-robustness", .string_int2(Self.encodeGrayscaleProgressive(_:_:), 56 | [ 57 | ("tests/integration/encode/karlie-kloss-1", (640, 320)) 58 | ])), 59 | ] 60 | } 61 | 62 | private static 63 | func print(image rgb:[JPEG.RGB], size:(x:Int, y:Int)) 64 | { 65 | for i:Int in stride(from: 0, to: size.y, by: 8) 66 | { 67 | let line:String = stride(from: 0, to: size.x, by: 8).map 68 | { 69 | (j:Int) in 70 | 71 | // downsampling 72 | var r:Int = 0, 73 | g:Int = 0, 74 | b:Int = 0 75 | for y:Int in i ..< min(i + 8, size.y) 76 | { 77 | for x:Int in j ..< min(j + 8, size.x) 78 | { 79 | let c:JPEG.RGB = rgb[x + y * size.x] 80 | r += .init(c.r) 81 | g += .init(c.g) 82 | b += .init(c.b) 83 | } 84 | } 85 | 86 | let count:Int = (min(i + 8, size.y) - i) * (min(j + 8, size.x) - j) 87 | let c:(r:Float, g:Float, b:Float) = 88 | ( 89 | .init(r) / (255 * .init(count)), 90 | .init(g) / (255 * .init(count)), 91 | .init(b) / (255 * .init(count)) 92 | ) 93 | return Highlight.square(c) 94 | }.joined(separator: "") 95 | Swift.print(line) 96 | } 97 | } 98 | 99 | // this test only tries to decode the image without errors, it does not check 100 | // for content accuracy 101 | static 102 | func decode(_ path:String) -> Result 103 | { 104 | do 105 | { 106 | guard let image:JPEG.Data.Rectangular = try .decompress(path: path) 107 | else 108 | { 109 | return .failure(.init(message: "failed to open file '\(path)'")) 110 | } 111 | 112 | Swift.print( 113 | """ 114 | 115 | \(Highlight.bold)\(path)\(Highlight.reset) (\(image.layout.format)) 116 | { 117 | size : (\(image.size.x), \(image.size.y)) 118 | process : \(image.layout.process) 119 | precision : \(image.layout.format.precision) 120 | components : 121 | [ 122 | \(image.layout.residents.sorted(by: { $0.key < $1.key }).map 123 | { 124 | let (x, y):(Int, Int) = 125 | image.layout.planes[$0.value].component.factor 126 | return "\($0.key): (\(x), \(y))" 127 | }.joined(separator: "\n ")) 128 | ] 129 | scans : 130 | [ 131 | \(image.layout.scans.map 132 | { 133 | "[band: \(String.pad("\($0.band.lowerBound)", left: 2)) ..< \(String.pad("\($0.band.upperBound)", left: 2)), bits: \(String.pad("\($0.bits.lowerBound)", left: 2)) \(String.pad($0.bits.upperBound == .max ? "..." : "..< \("\(String.pad("\($0.bits.upperBound)", left: 2))")", right: 6))]: \($0.components.map(\.ci))" 134 | }.joined(separator: "\n ")) 135 | ] 136 | } 137 | """) 138 | for metadata:JPEG.Metadata in image.metadata 139 | { 140 | switch metadata 141 | { 142 | case .jfif(let jfif): 143 | Swift.print(jfif) 144 | case .exif(let exif): 145 | Swift.print(exif) 146 | case .application(let a, data: let data): 147 | Swift.print("metadata (application \(a), \(data.count) bytes)") 148 | case .comment(data: let data): 149 | Swift.print(""" 150 | comment 151 | { 152 | '\(String.init(decoding: data, as: Unicode.UTF8.self))' 153 | } 154 | """) 155 | } 156 | } 157 | 158 | let rgb:[JPEG.RGB] = image.unpack(as: JPEG.RGB.self) 159 | // write to rgb file 160 | guard let _:Void = try (System.File.Destination.open(path: "\(path).rgb") 161 | { 162 | guard let _:Void = $0.write(rgb.flatMap{ [$0.r, $0.g, $0.b] }) 163 | else 164 | { 165 | throw Failure.init(message: "failed to write to file '\(path).rgb'") 166 | } 167 | }) 168 | else 169 | { 170 | throw Failure.init(message: "failed to open file '\(path).rgb'") 171 | } 172 | 173 | // terminal output 174 | Self.print(image: rgb, size: image.size) 175 | } 176 | catch 177 | { 178 | if let error:JPEG.Error = error as? JPEG.Error 179 | { 180 | return .failure(.init(message: error.message)) 181 | } 182 | else 183 | { 184 | return .failure(.init(message: "\(error)")) 185 | } 186 | } 187 | 188 | return .success(()) 189 | } 190 | 191 | static 192 | func encodeColorSequential(_ path:String, _ size:(x:Int, y:Int)) 193 | -> Result 194 | { 195 | let format:JPEG.Common = .ycc8 196 | let Y:JPEG.Component.Key = format.components[0], 197 | Cb:JPEG.Component.Key = format.components[1], 198 | Cr:JPEG.Component.Key = format.components[2] 199 | let layout:JPEG.Layout = .init( 200 | format: format, 201 | process: .baseline, 202 | components: 203 | [ 204 | Y: (factor: (1, 1), qi: 0), 205 | Cb: (factor: (1, 1), qi: 1), 206 | Cr: (factor: (1, 1), qi: 1), 207 | ], 208 | scans: 209 | [ 210 | .sequential((Y, \.0, \.0), (Cb, \.1, \.1), (Cr, \.1, \.1)) 211 | ]) 212 | return Self.encode(path, suffix: "-color-sequential", size: size, layout: layout) 213 | } 214 | static 215 | func encodeGrayscaleSequential(_ path:String, _ size:(x:Int, y:Int)) 216 | -> Result 217 | { 218 | let format:JPEG.Common = .y8 219 | let Y:JPEG.Component.Key = format.components[0] 220 | let layout:JPEG.Layout = .init( 221 | format: format, 222 | process: .baseline, 223 | components: 224 | [ 225 | Y: (factor: (1, 1), qi: 0), 226 | ], 227 | scans: 228 | [ 229 | .sequential((Y, \.0, \.0)) 230 | ]) 231 | return Self.encode(path, suffix: "-grayscale-sequential", size: size, layout: layout) 232 | } 233 | static 234 | func encodeColorProgressive(_ path:String, _ size:(x:Int, y:Int)) 235 | -> Result 236 | { 237 | let format:JPEG.Common = .ycc8 238 | let Y:JPEG.Component.Key = format.components[0], 239 | Cb:JPEG.Component.Key = format.components[1], 240 | Cr:JPEG.Component.Key = format.components[2] 241 | let layout:JPEG.Layout = .init( 242 | format: format, 243 | process: .progressive(coding: .huffman, differential: false), 244 | components: 245 | [ 246 | Y: (factor: (1, 1), qi: 0), 247 | Cb: (factor: (1, 1), qi: 1), 248 | Cr: (factor: (1, 1), qi: 1), 249 | ], 250 | scans: 251 | [ 252 | .progressive((Y, \.0), (Cb, \.1), (Cr, \.2), bits: 2...), 253 | .progressive( Y, Cb, Cr , bit: 1 ), 254 | .progressive( Y, Cb, Cr , bit: 0 ), 255 | 256 | .progressive((Y, \.0), band: 1 ..< 64, bits: 2...), 257 | 258 | .progressive((Cb, \.0), band: 1 ..< 6, bits: 1...), 259 | .progressive((Cr, \.0), band: 1 ..< 6, bits: 1...), 260 | 261 | .progressive((Cb, \.0), band: 6 ..< 64, bits: 1...), 262 | .progressive((Cr, \.0), band: 6 ..< 64, bits: 1...), 263 | 264 | .progressive((Y, \.0), band: 1 ..< 64, bit: 1 ), 265 | .progressive((Y, \.0), band: 1 ..< 64, bit: 0 ), 266 | .progressive((Cb, \.0), band: 1 ..< 64, bit: 0 ), 267 | .progressive((Cr, \.0), band: 1 ..< 64, bit: 0 ), 268 | ]) 269 | return Self.encode(path, suffix: "-color-progressive", size: size, layout: layout) 270 | } 271 | static 272 | func encodeGrayscaleProgressive(_ path:String, _ size:(x:Int, y:Int)) 273 | -> Result 274 | { 275 | let format:JPEG.Common = .y8 276 | let Y:JPEG.Component.Key = format.components[0] 277 | let layout:JPEG.Layout = .init( 278 | format: format, 279 | process: .progressive(coding: .huffman, differential: false), 280 | components: 281 | [ 282 | Y: (factor: (1, 1), qi: 0) 283 | ], 284 | scans: 285 | [ 286 | .progressive((Y, \.0), bits: 2...), 287 | .progressive( Y, bit: 1 ), 288 | .progressive( Y, bit: 0 ), 289 | 290 | .progressive((Y, \.0), band: 1 ..< 6, bits: 2...), 291 | .progressive((Y, \.0), band: 6 ..< 64, bits: 2...), 292 | .progressive((Y, \.0), band: 1 ..< 64, bit: 1 ), 293 | .progressive((Y, \.0), band: 1 ..< 64, bit: 0 ), 294 | ]) 295 | return Self.encode(path, suffix: "-grayscale-progressive", size: size, layout: layout) 296 | } 297 | private static 298 | func encode(_ path:String, suffix:String, size:(x:Int, y:Int), layout:JPEG.Layout) 299 | -> Result 300 | { 301 | do 302 | { 303 | guard let rgb:[JPEG.RGB] = try (System.File.Source.open(path: "\(path).rgb") 304 | { 305 | guard let data:[UInt8] = $0.read(count: 3 * size.x * size.y) 306 | else 307 | { 308 | throw Failure.init(message: "failed to read from file '\(path).rgb'") 309 | } 310 | 311 | return (0 ..< size.x * size.y).map 312 | { 313 | (i:Int) -> JPEG.RGB in 314 | .init(data[i * 3], data[i * 3 + 1], data[i * 3 + 2]) 315 | } 316 | }) 317 | else 318 | { 319 | throw Failure.init(message: "failed to open file '\(path).rgb'") 320 | } 321 | 322 | var planar:JPEG.Data.Planar = .init( 323 | size: size, 324 | layout: layout, 325 | metadata: 326 | [ 327 | .jfif(.init(version: .v1_2, density: (1, 1, .centimeters))), 328 | ]) 329 | 330 | let ycc:[JPEG.YCbCr] = rgb.map(\.ycc) 331 | for (ci, p):(JPEG.Component.Key, KeyPath) in 332 | zip(layout.format.components, [\.y, \.cb, \.cr]) 333 | { 334 | guard planar.index(forKey: ci) != nil 335 | else 336 | { 337 | continue 338 | } 339 | 340 | planar.with(ci: ci) 341 | { 342 | for (x, y):(Int, Int) in $0.indices 343 | { 344 | $0[x: x, y: y] = .init(ycc[x + y * size.x][keyPath: p]) 345 | } 346 | } 347 | } 348 | 349 | let quanta:([UInt16], [UInt16]) = 350 | ( 351 | (1 ... 64).map{ 1 + $0 >> 1 }, 352 | (1 ... 64).map{ 1 + 2 * ($0 & 0xfffe) } 353 | ) 354 | 355 | let spectral:JPEG.Data.Spectral = planar.fdct( 356 | quanta: 357 | [ 358 | 0: quanta.0, 359 | 1: quanta.1, 360 | ]) 361 | 362 | guard let _:Void = try spectral.compress(path: "\(path)\(suffix).jpg") 363 | else 364 | { 365 | fatalError("failed to open file '\(path)\(suffix).jpg'") 366 | } 367 | 368 | // terminal output 369 | let rectangular:JPEG.Data.Rectangular = 370 | spectral.idct().interleaved() 371 | Self.print(image: rectangular.unpack(as: JPEG.RGB.self), size: size) 372 | } 373 | catch 374 | { 375 | if let error:JPEG.Error = error as? JPEG.Error 376 | { 377 | return .failure(.init(message: error.message)) 378 | } 379 | else 380 | { 381 | return .failure(.init(message: "\(error)")) 382 | } 383 | } 384 | 385 | return .success(()) 386 | } 387 | } 388 | --------------------------------------------------------------------------------