├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── Directory.Build.targets ├── LICENSE ├── README.md ├── TinyPNG.sln ├── icon.png ├── src ├── .editorconfig ├── TinyPNG.Samples │ ├── Program.cs │ ├── Resources │ │ ├── cat.jpg │ │ ├── compressedcat.jpg │ │ └── resizedcat.jpg │ └── TinyPNG.Samples.csproj └── TinyPNG │ ├── AmazonS3Configuration.cs │ ├── CustomJsonStringEnumConverter.cs │ ├── Extensions │ ├── ConvertExtensions.cs │ ├── DownloadExtensions.cs │ ├── ImageDataExtensions.cs │ └── ResizeExtensions.cs │ ├── PreserveMetadata.cs │ ├── ResizeOperations │ ├── CoverResizeOperation.cs │ ├── FitResizeOperation.cs │ ├── ResizeOperation.cs │ ├── ScaleHeightResizeOperation.cs │ └── ScaleWidthResizeOperation.cs │ ├── Responses │ ├── ApiErrorResponse.cs │ ├── TinyPngCompressResponse.cs │ ├── TinyPngConvertResponse.cs │ ├── TinyPngImageResponse.cs │ ├── TinyPngResizeResponse.cs │ └── TinyPngResponse.cs │ ├── TinyPNG.csproj │ ├── TinyPngApiException.cs │ ├── TinyPngApiResult.cs │ └── TinyPngClient.cs └── tests └── TinyPng.Tests ├── Extensions.cs ├── FakeResponseHandler.cs ├── Resources ├── cat.jpg ├── compressedcat.jpg └── resizedcat.jpg ├── TinyPNG.Tests.csproj └── TinyPngTests.cs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ctolkien] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: TinyPNG Actions 2 | 3 | env: 4 | PATH_TO_CSPROJ: 'src/TinyPNG/TinyPNG.csproj' 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | 12 | workflow_dispatch: 13 | inputs: 14 | environment: 15 | description: 'Environment' 16 | type: environment 17 | required: true 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | name: Build 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Setup .NET 26 | uses: actions/setup-dotnet@v4 27 | with: 28 | dotnet-version: 8.0.x 29 | - name: Restore dependencies 30 | run: dotnet restore 31 | - name: Build 32 | run: dotnet build --no-restore 33 | - name: Test 34 | run: dotnet test --no-build --verbosity normal --logger GitHubActions 35 | - name: Pack #If we're not in production, add a version suffix to the package. This indicates pre-release 36 | if: inputs.environment != 'Production' 37 | run: dotnet pack ./src/TinyPNG --configuration release --output ${{ github.workspace}}/artifact/ /p:VersionSuffix=prerelease.${{ github.run_number}} 38 | - name: Pack 39 | if: inputs.environment == 'Production' 40 | run: dotnet pack ./src/TinyPNG --configuration release --output ${{ github.workspace}}/artifact/ 41 | 42 | - name: Upload artifact 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: tinypng 46 | path: ${{ github.workspace}}/artifact/ 47 | if-no-files-found: error 48 | - name: Push to GitHub Package Registry 49 | run: dotnet nuget push ${{ github.workspace}}/artifact/*.nupkg --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --api-key ${{secrets.GITHUB_TOKEN}} --skip-duplicate 50 | 51 | deploy: 52 | runs-on: ubuntu-latest 53 | needs: build 54 | name: Deploy 55 | environment: ${{ inputs.environment }} 56 | if: (inputs.environment == 'Production') || (inputs.environment == 'PreRelease') 57 | steps: 58 | - uses: actions/checkout@v3 59 | - uses: actions/download-artifact@v4 60 | with: 61 | name: tinypng 62 | path: ${{ github.workspace}}/artifact/ 63 | - name: Push to NuGet Package Registry 64 | run: dotnet nuget push ${{ github.workspace}}/artifact/*.nupkg --api-key ${{ secrets.NUGET_APIKEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate 65 | 66 | - name: Extract version 67 | shell: pwsh 68 | run: | 69 | $version = ([xml](Get-Content ${{ env.PATH_TO_CSPROJ }})).Project.PropertyGroup.VersionPrefix[0].Trim() 70 | Add-Content -Path $env:GITHUB_ENV -Value "VERSION=$version" 71 | Add-Content -Path $env:GITHUB_ENV -Value "VERSIONWITHSUFFIX=$version-${{ github.run_number }}" 72 | 73 | - name: Create GitHub Release 74 | if: inputs.environment == 'PreRelease' 75 | run: gh release create "${{ env.VERSIONWITHSUFFIX }}" --generate-notes --prerelease 76 | env: 77 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 78 | - name: Create GitHub Release 79 | if: inputs.environment == 'Production' 80 | run: gh release create "${{ env.VERSION }}" --generate-notes 81 | env: 82 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 83 | 84 | 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | .ncrunchsolution 106 | 107 | # MightyMoose 108 | *.mm.* 109 | AutoTest.Net/ 110 | 111 | # Web workbench (sass) 112 | .sass-cache/ 113 | 114 | # Installshield output folder 115 | [Ee]xpress/ 116 | 117 | # DocProject is a documentation generator add-in 118 | DocProject/buildhelp/ 119 | DocProject/Help/*.HxT 120 | DocProject/Help/*.HxC 121 | DocProject/Help/*.hhc 122 | DocProject/Help/*.hhk 123 | DocProject/Help/*.hhp 124 | DocProject/Help/Html2 125 | DocProject/Help/html 126 | 127 | # Click-Once directory 128 | publish/ 129 | 130 | # Publish Web Output 131 | *.[Pp]ublish.xml 132 | *.azurePubxml 133 | # TODO: Comment the next line if you want to checkin your web deploy settings 134 | # but database connection strings (with potential passwords) will be unencrypted 135 | *.pubxml 136 | *.publishproj 137 | 138 | # NuGet Packages 139 | *.nupkg 140 | # The packages folder can be ignored because of Package Restore 141 | **/packages/* 142 | # except build/, which is used as an MSBuild target. 143 | !**/packages/build/ 144 | # Uncomment if necessary however generally it will be regenerated when needed 145 | #!**/packages/repositories.config 146 | 147 | # Windows Azure Build Output 148 | csx/ 149 | *.build.csdef 150 | 151 | # Windows Store app package directory 152 | AppPackages/ 153 | 154 | # Others 155 | *.[Cc]ache 156 | ClientBin/ 157 | [Ss]tyle[Cc]op.* 158 | ~$* 159 | *~ 160 | *.dbmdl 161 | *.dbproj.schemaview 162 | *.pfx 163 | *.publishsettings 164 | node_modules/ 165 | bower_components/ 166 | 167 | # RIA/Silverlight projects 168 | Generated_Code/ 169 | 170 | # Backup & report files from converting an old project file 171 | # to a newer Visual Studio version. Backup files are not needed, 172 | # because we have git ;-) 173 | _UpgradeReport_Files/ 174 | Backup*/ 175 | UpgradeLog*.XML 176 | UpgradeLog*.htm 177 | 178 | # SQL Server files 179 | *.mdf 180 | *.ldf 181 | 182 | # Business Intelligence projects 183 | *.rdl.data 184 | *.bim.layout 185 | *.bim_*.settings 186 | 187 | # Microsoft Fakes 188 | FakesAssemblies/ 189 | 190 | # Node.js Tools for Visual Studio 191 | .ntvs_analysis.dat 192 | 193 | # Visual Studio 6 build log 194 | *.plg 195 | 196 | # Visual Studio 6 workspace options file 197 | *.opt 198 | 199 | #VS Code 200 | .vscode 201 | 202 | #Rider 203 | .idea -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chad T 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TinyPng for .NET 2 | 3 | [![TinyPNG on NuGet](https://img.shields.io/nuget/v/tinypng.svg?maxAge=2000)](https://www.nuget.org/packages/TinyPNG) 4 | [![MIT license](https://img.shields.io/github/license/ctolkien/TinyPNG.svg?maxAge=2592000)](LICENSE) 5 | 6 | This is a .NET Standard wrapper around the [TinyPNG.com](https://tinypng.com) image compression service. This is not an official TinyPNG.com product. 7 | 8 | * Supports .NET Core and full .NET Framework 9 | * Non-blocking async turtles all the way down 10 | * `Byte[]`, `Stream`, `File` and `Url` API's available 11 | 12 | ## Installation 13 | 14 | Install via Nuget 15 | 16 | ``` 17 | Install-Package TinyPNG 18 | ``` 19 | 20 | Install via `dotnet` 21 | 22 | ``` 23 | dotnet add package TinyPNG 24 | ``` 25 | 26 | ## Quickstart 27 | ```csharp 28 | using var png = new TinyPngClient("yourSecretApiKey"); 29 | var result = await png.Compress("cat.jpg"); 30 | 31 | //URL to your compressed version 32 | result.Output.Url; 33 | ``` 34 | 35 | # Version Upgrades 36 | 37 | ## Upgrading from V3 to V4 38 | 39 | * The namespaces have changed for the extension methods to all reside in the `TinyPng` namespace. This will avoid needing to bring in two different namespaces. 40 | * The `CompressFromUrl` method has been removed. This is now available via a new overload for `Compress` which takes in a `Uri` object 41 | * I've standardised the namespace on `TinyPng`, it was a bit of a mixed bag of casing previously. 42 | 43 | ## Upgrading from V2 to V3 44 | 45 | The API has changed from V2, primarily you no longer need to await each individual 46 | step of using the TinyPNG API, you can now chain appropriate calls together as 47 | the extension methods now operate on `Task`. 48 | 49 | ## Compressing Images 50 | 51 | ```csharp 52 | // create an instance of the TinyPngClient 53 | using var png = new TinyPngClient("yourSecretApiKey"); 54 | 55 | // Create a task to compress an image. 56 | // this gives you the information about your image as stored by TinyPNG 57 | // they don't give you the actual bits (as you may want to chain this with a resize 58 | // operation without caring for the originally sized image). 59 | var compressImageTask = png.Compress("pathToFile or byte array or stream"); 60 | // or `CompressFromUrl` if compressing from a remotely hosted image. 61 | var compressFromUrlImageTask = png.CompressFromUrl("image url"); 62 | 63 | // If you want to actually save this compressed image off 64 | // it will need to be downloaded 65 | var compressedImage = await compressImageTask.Download(); 66 | 67 | // you can then get the bytes 68 | var bytes = await compressedImage.GetImageByteData(); 69 | 70 | // get a stream instead 71 | var stream = await compressedImage.GetImageStreamData(); 72 | 73 | // or just save to disk 74 | await compressedImage.SaveImageToDisk("pathToSaveImage"); 75 | 76 | // Putting it all together 77 | await png.Compress("path") 78 | .Download() 79 | .SaveImageToDisk("savedPath"); 80 | ``` 81 | 82 | Further details about the result of the compression are also available on the `Input` and `Output` properties of a `Compress` operation. Some examples: 83 | ```csharp 84 | var result = await png.Compress("pathToFile or byte array or stream"); 85 | 86 | // old size 87 | result.Input.Size; 88 | 89 | // new size 90 | result.Output.Size; 91 | 92 | // URL of the compressed Image 93 | result.Output.Url; 94 | ``` 95 | 96 | ## Resizing Images 97 | 98 | ```csharp 99 | using var png = new TinyPngClient("yourSecretApiKey"); 100 | 101 | var compressImageTask = png.Compress("pathToFile or byte array or stream"); 102 | 103 | var resizedImageTask = compressImageTask.Resize(width, height); 104 | 105 | await resizedImageTask.SaveImageToDisk("pathToSaveImage"); 106 | 107 | // altogether now.... 108 | await png.Compress("pathToFile") 109 | .Resize(width, height) 110 | .SaveImageToDisk("pathToSaveImage"); 111 | ``` 112 | 113 | ### Resize Operations 114 | 115 | There are certain combinations when specifying resize options which aren't compatible with 116 | TinyPNG. We also include strongly typed resize operations, 117 | depending on the type of resize you want to do. 118 | 119 | ```csharp 120 | using var png = new TinyPngClient("yourSecretApiKey"); 121 | 122 | var compressTask = png.Compress("pathToFile or byte array or stream"); 123 | 124 | await compressTask.Resize(new ScaleWidthResizeOperation(width)); 125 | await compressTask.Resize(new ScaleHeightResizeOperation(height)); 126 | await compressTask.Resize(new FitResizeOperation(width, height)); 127 | await compressTask.Resize(new CoverResizeOperation(width, height)); 128 | ``` 129 | 130 | The same `Byte[]`, `Stream`, `File` and `Url` path API's are available from the result of the `Resize()` method. 131 | 132 | ## Converting Formats (v4) 133 | 134 | You can convert images to different formats using the `Convert()` method. This will return a object which contains the converted image data. 135 | 136 | ```csharp 137 | using var png = new TinyPngClient("yourSecretApiKey"); 138 | 139 | var compressAndConvert = await png.Compress("cat.png").Convert(ConvertImageFormat.Wildcard); 140 | ``` 141 | 142 | By using the `Wildcard` format, TinyPng will return the best type for the supplied image. 143 | 144 | In the scenario that you are converting to an image and losing transparency, you can specify a background colour to use for the image. 145 | 146 | ```csharp 147 | var compressAndConvert = await png.Compress("cat.png").Convert(ConvertImageFormat.Wildcard, "#FF0000"); 148 | ``` 149 | 150 | 151 | ## Amazon S3 Storage 152 | 153 | The result of any compress operation can be stored directly on to Amazon S3 storage. I'd strongly recommend referring to [TinyPNG.com's documentation](https://tinypng.com/developers/reference) with regard to how to configure 154 | the appropriate S3 access. 155 | 156 | If you're going to be storing images for most requests into S3, then you can pass in an `AmazonS3Configuration` object to the constructor which will be used for all subsequent requests. 157 | 158 | ```csharp 159 | using var png = new TinyPngClient("yourSecretApiKey", 160 | new AmazonS3Configuration("awsAccessKeyId", "awsSecretAccessKey", "bucket", "region")); 161 | 162 | var compressedCat = await png.Compress("cat.jpg"); 163 | var s3Uri = await png.SaveCompressedImageToAmazonS3(compressedCat, "file-name.png"); 164 | 165 | // If you'd like to override the particular bucket or region 166 | // an image is being stored to from what is specified in the AmazonS3Configuration: 167 | var s3UriInNewSpot = await png.SaveCompressedImageToAmazonS3( 168 | compressedCat, 169 | "file-name.png", 170 | bucketOverride: "different-bucket", 171 | regionOverride: "different-region"); 172 | ``` 173 | 174 | You can also pass a `AmazonS3Configuration` object directly into calls to `SaveCompressedImageToAmazonS3` 175 | 176 | ```csharp 177 | using var png = new TinyPngClient("yourSecretApiKey"); 178 | var compressedCat = await png.Compress("cat.jpg"); 179 | var s3Uri = await png.SaveCompressedImageToAmazonS3(compressedCat, 180 | new AmazonS3Configuration( 181 | "awsAccessKeyId", 182 | "awsSecretAccessKey", 183 | "bucket", 184 | "region"), "file-name.png"); 185 | ``` 186 | 187 | 188 | ## Compression Count 189 | 190 | You can get a read on the number of compression operations you've performed by inspecting the `CompressionCount` property 191 | on the result of any operation you've performed. This is useful for keeping tabs on your API usage. 192 | 193 | ```csharp 194 | var compressedCat = await png.Compress("cat.jpg"); 195 | compressedCat.CompressionCount; // = 5 196 | ``` 197 | 198 | ## HttpClient 199 | 200 | TinyPngClient can take HttpClient as constructor overload, the lifetime of which can be controlled from outside the library. 201 | 202 | ```csharp 203 | var httpClient = new HttpClient(); 204 | var png = new TinyPngClient("yourSecretApiKey", httpClient); 205 | ``` 206 | -------------------------------------------------------------------------------- /TinyPNG.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.6.33712.159 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TinyPNG", "src\TinyPNG\TinyPNG.csproj", "{52901FDB-68CE-4192-8B2B-3BD1773261F3}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TinyPNG.Tests", "tests\TinyPng.Tests\TinyPNG.Tests.csproj", "{DBA34C0E-C2A8-4D42-8770-FADC6251E93D}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{466E1CAF-57A2-4370-91C7-455AAC8E4D2E}" 10 | ProjectSection(SolutionItems) = preProject 11 | .gitignore = .gitignore 12 | Directory.Build.targets = Directory.Build.targets 13 | icon.png = icon.png 14 | LICENSE = LICENSE 15 | .github\workflows\main.yml = .github\workflows\main.yml 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TinyPNG.Samples", "src\TinyPNG.Samples\TinyPNG.Samples.csproj", "{F4BE9305-126D-49CF-830F-E840A337EF13}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {52901FDB-68CE-4192-8B2B-3BD1773261F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {52901FDB-68CE-4192-8B2B-3BD1773261F3}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {52901FDB-68CE-4192-8B2B-3BD1773261F3}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {52901FDB-68CE-4192-8B2B-3BD1773261F3}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {DBA34C0E-C2A8-4D42-8770-FADC6251E93D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {DBA34C0E-C2A8-4D42-8770-FADC6251E93D}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {DBA34C0E-C2A8-4D42-8770-FADC6251E93D}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {DBA34C0E-C2A8-4D42-8770-FADC6251E93D}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {F4BE9305-126D-49CF-830F-E840A337EF13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {F4BE9305-126D-49CF-830F-E840A337EF13}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {F4BE9305-126D-49CF-830F-E840A337EF13}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {F4BE9305-126D-49CF-830F-E840A337EF13}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(SolutionProperties) = preSolution 41 | HideSolutionNode = FALSE 42 | EndGlobalSection 43 | GlobalSection(ExtensibilityGlobals) = postSolution 44 | SolutionGuid = {54A4A777-ED82-4D34-A6E1-AACC3E946A40} 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctolkien/TinyPNG/3ebe83852ba0c45a224c531ce7da6b864cdfd229/icon.png -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = false 23 | file_header_template = unset 24 | 25 | # this. and Me. preferences 26 | dotnet_style_qualification_for_event = false 27 | dotnet_style_qualification_for_field = false 28 | dotnet_style_qualification_for_method = false 29 | dotnet_style_qualification_for_property = false 30 | 31 | # Language keywords vs BCL types preferences 32 | dotnet_style_predefined_type_for_locals_parameters_members = true 33 | dotnet_style_predefined_type_for_member_access = true 34 | 35 | # Parentheses preferences 36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity 37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity 38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary 39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity 40 | 41 | # Modifier preferences 42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members 43 | 44 | # Expression-level preferences 45 | dotnet_style_coalesce_expression = true 46 | dotnet_style_collection_initializer = true 47 | dotnet_style_explicit_tuple_names = true 48 | dotnet_style_namespace_match_folder = true 49 | dotnet_style_null_propagation = true 50 | dotnet_style_object_initializer = true 51 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 52 | dotnet_style_prefer_auto_properties = true:suggestion 53 | dotnet_style_prefer_collection_expression = when_types_loosely_match 54 | dotnet_style_prefer_compound_assignment = true 55 | dotnet_style_prefer_conditional_expression_over_assignment = true 56 | dotnet_style_prefer_conditional_expression_over_return = true 57 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed 58 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 59 | dotnet_style_prefer_inferred_tuple_names = true 60 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 61 | dotnet_style_prefer_simplified_boolean_expressions = true 62 | dotnet_style_prefer_simplified_interpolation = true 63 | 64 | # Field preferences 65 | dotnet_style_readonly_field = true 66 | 67 | # Parameter preferences 68 | dotnet_code_quality_unused_parameters = all 69 | 70 | # Suppression preferences 71 | dotnet_remove_unnecessary_suppression_exclusions = none 72 | 73 | # New line preferences 74 | dotnet_style_allow_multiple_blank_lines_experimental = true 75 | dotnet_style_allow_statement_immediately_after_block_experimental = true 76 | 77 | #### C# Coding Conventions #### 78 | 79 | # var preferences 80 | csharp_style_var_elsewhere = false 81 | csharp_style_var_for_built_in_types = false 82 | csharp_style_var_when_type_is_apparent = false 83 | 84 | # Expression-bodied members 85 | csharp_style_expression_bodied_accessors = true 86 | csharp_style_expression_bodied_constructors = false 87 | csharp_style_expression_bodied_indexers = true 88 | csharp_style_expression_bodied_lambdas = true 89 | csharp_style_expression_bodied_local_functions = false 90 | csharp_style_expression_bodied_methods = false 91 | csharp_style_expression_bodied_operators = false 92 | csharp_style_expression_bodied_properties = true 93 | 94 | # Pattern matching preferences 95 | csharp_style_pattern_matching_over_as_with_null_check = true 96 | csharp_style_pattern_matching_over_is_with_cast_check = true 97 | csharp_style_prefer_extended_property_pattern = true 98 | csharp_style_prefer_not_pattern = true 99 | csharp_style_prefer_pattern_matching = true 100 | csharp_style_prefer_switch_expression = true 101 | 102 | # Null-checking preferences 103 | csharp_style_conditional_delegate_call = true 104 | 105 | # Modifier preferences 106 | csharp_prefer_static_local_function = true 107 | csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async 108 | csharp_style_prefer_readonly_struct = true 109 | csharp_style_prefer_readonly_struct_member = true 110 | 111 | # Code-block preferences 112 | csharp_prefer_braces = true 113 | csharp_prefer_simple_using_statement = true 114 | csharp_style_namespace_declarations = file_scoped 115 | csharp_style_prefer_method_group_conversion = true 116 | csharp_style_prefer_primary_constructors = true 117 | csharp_style_prefer_top_level_statements = true 118 | 119 | # Expression-level preferences 120 | csharp_prefer_simple_default_expression = true 121 | csharp_style_deconstructed_variable_declaration = true 122 | csharp_style_implicit_object_creation_when_type_is_apparent = true 123 | csharp_style_inlined_variable_declaration = true 124 | csharp_style_prefer_index_operator = true 125 | csharp_style_prefer_local_over_anonymous_function = true 126 | csharp_style_prefer_null_check_over_type_check = true 127 | csharp_style_prefer_range_operator = true 128 | csharp_style_prefer_tuple_swap = true 129 | csharp_style_prefer_utf8_string_literals = true 130 | csharp_style_throw_expression = true 131 | csharp_style_unused_value_assignment_preference = discard_variable 132 | csharp_style_unused_value_expression_statement_preference = discard_variable 133 | 134 | # 'using' directive preferences 135 | csharp_using_directive_placement = outside_namespace 136 | 137 | # New line preferences 138 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true 139 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true 140 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true 141 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true 142 | csharp_style_allow_embedded_statements_on_same_line_experimental = true 143 | 144 | #### C# Formatting Rules #### 145 | 146 | # New line preferences 147 | csharp_new_line_before_catch = true 148 | csharp_new_line_before_else = true 149 | csharp_new_line_before_finally = true 150 | csharp_new_line_before_members_in_anonymous_types = true 151 | csharp_new_line_before_members_in_object_initializers = true 152 | csharp_new_line_before_open_brace = all 153 | csharp_new_line_between_query_expression_clauses = true 154 | 155 | # Indentation preferences 156 | csharp_indent_block_contents = true 157 | csharp_indent_braces = false 158 | csharp_indent_case_contents = true 159 | csharp_indent_case_contents_when_block = true 160 | csharp_indent_labels = one_less_than_current 161 | csharp_indent_switch_labels = true 162 | 163 | # Space preferences 164 | csharp_space_after_cast = false 165 | csharp_space_after_colon_in_inheritance_clause = true 166 | csharp_space_after_comma = true 167 | csharp_space_after_dot = false 168 | csharp_space_after_keywords_in_control_flow_statements = true 169 | csharp_space_after_semicolon_in_for_statement = true 170 | csharp_space_around_binary_operators = before_and_after 171 | csharp_space_around_declaration_statements = false 172 | csharp_space_before_colon_in_inheritance_clause = true 173 | csharp_space_before_comma = false 174 | csharp_space_before_dot = false 175 | csharp_space_before_open_square_brackets = false 176 | csharp_space_before_semicolon_in_for_statement = false 177 | csharp_space_between_empty_square_brackets = false 178 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 179 | csharp_space_between_method_call_name_and_opening_parenthesis = false 180 | csharp_space_between_method_call_parameter_list_parentheses = false 181 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 182 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 183 | csharp_space_between_method_declaration_parameter_list_parentheses = false 184 | csharp_space_between_parentheses = false 185 | csharp_space_between_square_brackets = false 186 | 187 | # Wrapping preferences 188 | csharp_preserve_single_line_blocks = true 189 | csharp_preserve_single_line_statements = true 190 | 191 | #### Naming styles #### 192 | 193 | # Naming rules 194 | 195 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 196 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 197 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 198 | 199 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 200 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 201 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 202 | 203 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 204 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 205 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 206 | 207 | dotnet_naming_rule.private_or_internal_field_should_be_starts_with_underscore.severity = suggestion 208 | dotnet_naming_rule.private_or_internal_field_should_be_starts_with_underscore.symbols = private_or_internal_field 209 | dotnet_naming_rule.private_or_internal_field_should_be_starts_with_underscore.style = starts_with_underscore 210 | 211 | # Symbol specifications 212 | 213 | dotnet_naming_symbols.interface.applicable_kinds = interface 214 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 215 | dotnet_naming_symbols.interface.required_modifiers = 216 | 217 | dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field 218 | dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected 219 | dotnet_naming_symbols.private_or_internal_field.required_modifiers = 220 | 221 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 222 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 223 | dotnet_naming_symbols.types.required_modifiers = 224 | 225 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 226 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 227 | dotnet_naming_symbols.non_field_members.required_modifiers = 228 | 229 | # Naming styles 230 | 231 | dotnet_naming_style.pascal_case.required_prefix = 232 | dotnet_naming_style.pascal_case.required_suffix = 233 | dotnet_naming_style.pascal_case.word_separator = 234 | dotnet_naming_style.pascal_case.capitalization = pascal_case 235 | 236 | dotnet_naming_style.begins_with_i.required_prefix = I 237 | dotnet_naming_style.begins_with_i.required_suffix = 238 | dotnet_naming_style.begins_with_i.word_separator = 239 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 240 | 241 | dotnet_naming_style.starts_with_underscore.required_prefix = _ 242 | dotnet_naming_style.starts_with_underscore.required_suffix = 243 | dotnet_naming_style.starts_with_underscore.word_separator = 244 | dotnet_naming_style.starts_with_underscore.capitalization = camel_case 245 | -------------------------------------------------------------------------------- /src/TinyPNG.Samples/Program.cs: -------------------------------------------------------------------------------- 1 | using TinyPng; 2 | 3 | 4 | var tinyPngClient = new TinyPngClient("lolwat"); 5 | 6 | //var response = await tinyPngClient.Compress(@"./Resources/cat.jpg"); 7 | //var x = await tinyPngClient.Compress(@"./Resources/cat.jpg").Resize(100, 100); 8 | //var y = await tinyPngClient.Compress(@"./Resources/cat.jpg").Download().GetImageByteData(); 9 | 10 | var q = await tinyPngClient.Compress(@"./Resources/cat.jpg").Convert(ConvertImageFormat.Wildcard); 11 | 12 | //Console.WriteLine($"Compression Count {x.CompressionCount}"); 13 | //Console.WriteLine($"Byte length {y.Length}"); 14 | 15 | Console.WriteLine($"Converted type = {q.ContentType}"); 16 | 17 | Console.ReadKey(); -------------------------------------------------------------------------------- /src/TinyPNG.Samples/Resources/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctolkien/TinyPNG/3ebe83852ba0c45a224c531ce7da6b864cdfd229/src/TinyPNG.Samples/Resources/cat.jpg -------------------------------------------------------------------------------- /src/TinyPNG.Samples/Resources/compressedcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctolkien/TinyPNG/3ebe83852ba0c45a224c531ce7da6b864cdfd229/src/TinyPNG.Samples/Resources/compressedcat.jpg -------------------------------------------------------------------------------- /src/TinyPNG.Samples/Resources/resizedcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctolkien/TinyPNG/3ebe83852ba0c45a224c531ce7da6b864cdfd229/src/TinyPNG.Samples/Resources/resizedcat.jpg -------------------------------------------------------------------------------- /src/TinyPNG.Samples/TinyPNG.Samples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/TinyPNG/AmazonS3Configuration.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace TinyPng 4 | { 5 | public class AmazonS3Configuration(string awsAccessKeyId, 6 | string awsSecretAccessKey, 7 | string defaultBucket, 8 | string defaultRegion) 9 | { 10 | [JsonPropertyName("service")] 11 | public const string Service = "s3"; 12 | 13 | [JsonPropertyName("aws_access_key_id")] 14 | public string AwsAccessKeyId { get; } = awsAccessKeyId; 15 | [JsonPropertyName("aws_secret_access_key")] 16 | public string AwsSecretAccessKey { get; } = awsSecretAccessKey; 17 | public string Region { get; set; } = defaultRegion; 18 | [JsonIgnore] 19 | public string Bucket { get; set; } = defaultBucket; 20 | [JsonIgnore] 21 | public string Path { get; set; } 22 | 23 | [JsonPropertyName("path")] 24 | public string BucketPath 25 | { 26 | get 27 | { 28 | return $"{Bucket}/{Path}"; 29 | } 30 | } 31 | 32 | public AmazonS3Configuration Clone() 33 | { 34 | return new AmazonS3Configuration(AwsAccessKeyId, AwsSecretAccessKey, Bucket, Region); 35 | } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/TinyPNG/CustomJsonStringEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Runtime.Serialization; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization; 8 | 9 | namespace TinyPng 10 | { 11 | /// 12 | /// Pinched from https://stackoverflow.com/questions/59059989/system-text-json-how-do-i-specify-a-custom-name-for-an-enum-value 13 | /// This is because System.Text.Json doesn't support EnumMemberAttribute or a way to customise what an Enum value is serialised as. 14 | /// 15 | internal class CustomJsonStringEnumConverter(JsonNamingPolicy namingPolicy = null, bool allowIntegerValues = true) : JsonConverterFactory 16 | { 17 | private readonly JsonStringEnumConverter _baseConverter = new(namingPolicy, allowIntegerValues); 18 | 19 | public CustomJsonStringEnumConverter() : this(null, true) { } 20 | 21 | public override bool CanConvert(Type typeToConvert) => _baseConverter.CanConvert(typeToConvert); 22 | 23 | public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) 24 | { 25 | var query = from field in typeToConvert.GetFields(BindingFlags.Public | BindingFlags.Static) 26 | let attr = field.GetCustomAttribute() 27 | where attr != null 28 | select (field.Name, attr.Value); 29 | var dictionary = query.ToDictionary(p => p.Name, p => p.Value); 30 | if (dictionary.Count > 0) 31 | { 32 | return new JsonStringEnumConverter(new DictionaryLookupNamingPolicy(dictionary, namingPolicy), allowIntegerValues).CreateConverter(typeToConvert, options); 33 | } 34 | else 35 | { 36 | return _baseConverter.CreateConverter(typeToConvert, options); 37 | } 38 | } 39 | } 40 | 41 | public class JsonNamingPolicyDecorator(JsonNamingPolicy underlyingNamingPolicy) : JsonNamingPolicy 42 | { 43 | public override string ConvertName(string name) => underlyingNamingPolicy == null ? name : underlyingNamingPolicy.ConvertName(name); 44 | } 45 | 46 | internal class DictionaryLookupNamingPolicy(Dictionary dictionary, JsonNamingPolicy underlyingNamingPolicy) : JsonNamingPolicyDecorator(underlyingNamingPolicy) 47 | { 48 | readonly Dictionary _dictionary = dictionary ?? throw new ArgumentNullException(); 49 | 50 | public override string ConvertName(string name) => _dictionary.TryGetValue(name, out var value) ? value : base.ConvertName(name); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/TinyPNG/Extensions/ConvertExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Runtime.Serialization; 4 | using System.Text; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using TinyPng.Responses; 8 | 9 | namespace TinyPng; 10 | 11 | public static class ConvertExtensions 12 | { 13 | /// 14 | /// Convert the image into another format 15 | /// 16 | /// 17 | /// 18 | /// Optional. Specify a hex value such as #000FFF when converting to a non-transparent image option 19 | /// You must specify a background color if you wish to convert an image with a transparent background to an image type which does not support transparency (like JPEG). 20 | /// 21 | /// 22 | /// 23 | /// 24 | public static async Task Convert(this Task result, ConvertImageFormat convertOperation, string backgroundTransform = null) 25 | { 26 | if (result == null) 27 | { 28 | throw new ArgumentNullException(nameof(result)); 29 | } 30 | if (!string.IsNullOrEmpty(backgroundTransform) && (!backgroundTransform.StartsWith("#") || backgroundTransform.Length != 7)) 31 | { 32 | throw new ArgumentOutOfRangeException(nameof(backgroundTransform), $"If {nameof(backgroundTransform)} is supplied, it should be a 6 character hex value, and include the hash"); 33 | } 34 | 35 | TinyPngCompressResponse compressResponse = await result; 36 | 37 | var requestBody = JsonSerializer.Serialize( 38 | new { 39 | convert = new { type = convertOperation }, 40 | transform = !string.IsNullOrEmpty(backgroundTransform) ? new { background = backgroundTransform } : null 41 | }, 42 | TinyPngClient._jsonOptions); 43 | 44 | HttpRequestMessage msg = new(HttpMethod.Post, compressResponse.Output.Url) 45 | { 46 | Content = new StringContent(requestBody, Encoding.UTF8, "application/json") 47 | }; 48 | 49 | HttpResponseMessage response = await compressResponse._httpClient.SendAsync(msg); 50 | if (response.IsSuccessStatusCode) 51 | { 52 | return new TinyPngConvertResponse(response); 53 | } 54 | 55 | ApiErrorResponse errorMsg = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), TinyPngClient._jsonOptions); 56 | throw new TinyPngApiException((int)response.StatusCode, response.ReasonPhrase, errorMsg.Error, errorMsg.Message); 57 | 58 | 59 | } 60 | } 61 | 62 | 63 | public enum ConvertImageFormat 64 | { 65 | /// 66 | /// By using wildcard, TinyPng will return the best format for the image. 67 | /// 68 | [EnumMember(Value = "*/*")] 69 | Wildcard, 70 | [EnumMember(Value = "image/webp")] 71 | WebP, 72 | [EnumMember(Value = "image/jpeg")] 73 | Jpeg, 74 | [EnumMember(Value = "image/png")] 75 | Png 76 | } 77 | -------------------------------------------------------------------------------- /src/TinyPNG/Extensions/DownloadExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using TinyPng.Responses; 7 | 8 | namespace TinyPng; 9 | 10 | public static class DownloadExtensions 11 | { 12 | private const string _jpegType = "image/jpeg"; 13 | 14 | /// 15 | /// Downloads the result of a TinyPng Compression operation 16 | /// 17 | /// 18 | /// 19 | /// 20 | public static async Task Download(this Task compressResponse, PreserveMetadata metadata = PreserveMetadata.None) 21 | { 22 | if (compressResponse == null) 23 | throw new ArgumentNullException(nameof(compressResponse)); 24 | 25 | var compressResult = await compressResponse; 26 | 27 | return await Download(compressResult, metadata); 28 | } 29 | 30 | /// 31 | /// Downloads the result of a TinyPng Compression operation 32 | /// 33 | /// 34 | /// 35 | /// Thrown if you attempt to preserve metadata on an unsupported filetype 36 | /// 37 | public static async Task Download(this TinyPngCompressResponse compressResponse, PreserveMetadata metadata = PreserveMetadata.None) 38 | { 39 | if (compressResponse == null) 40 | throw new ArgumentNullException(nameof(compressResponse)); 41 | 42 | var msg = new HttpRequestMessage(HttpMethod.Get, compressResponse.Output.Url) 43 | { 44 | Content = CreateContent(metadata, compressResponse.Output.Type) 45 | }; 46 | 47 | var response = await compressResponse._httpClient.SendAsync(msg).ConfigureAwait(false); 48 | 49 | if (response.IsSuccessStatusCode) 50 | { 51 | return new TinyPngImageResponse(response); 52 | } 53 | 54 | var errorMsg = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync()); 55 | throw new TinyPngApiException((int)response.StatusCode, response.ReasonPhrase, errorMsg.Error, errorMsg.Message); 56 | } 57 | 58 | private static HttpContent CreateContent(PreserveMetadata metadata, string type) 59 | { 60 | if (metadata == PreserveMetadata.None) 61 | return null; 62 | 63 | var preserve = new List(); 64 | 65 | if (metadata.HasFlag(PreserveMetadata.Copyright)) 66 | { 67 | preserve.Add("copyright"); 68 | } 69 | if (metadata.HasFlag(PreserveMetadata.Creation)) 70 | { 71 | if (type != _jpegType) 72 | throw new InvalidOperationException($"Creation metadata can only be preserved with type {_jpegType}"); 73 | 74 | preserve.Add("creation"); 75 | } 76 | if (metadata.HasFlag(PreserveMetadata.Location)) 77 | { 78 | if (type != _jpegType) 79 | throw new InvalidOperationException($"Location metadata can only be preserved with type {_jpegType}"); 80 | 81 | preserve.Add("location"); 82 | } 83 | 84 | var json = JsonSerializer.Serialize(new { preserve }); 85 | 86 | return new StringContent(json, System.Text.Encoding.UTF8, "application/json"); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /src/TinyPNG/Extensions/ImageDataExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using TinyPng.Responses; 4 | 5 | namespace TinyPng; 6 | 7 | public static class ImageDataExtensions 8 | { 9 | /// 10 | /// Get the image data as a byte array 11 | /// 12 | /// The result from compress 13 | /// Byte array of the image data 14 | public static async Task GetImageByteData(this Task result) where T : TinyPngImageResponse 15 | { 16 | var imageResponse = await result; 17 | return await imageResponse.GetImageByteData(); 18 | } 19 | 20 | /// 21 | /// Get the image data as a byte array 22 | /// 23 | /// The result from compress 24 | /// Byte array of the image data 25 | public static async Task GetImageByteData(this TinyPngImageResponse result) 26 | { 27 | return await result.HttpResponseMessage.Content.ReadAsByteArrayAsync(); 28 | } 29 | 30 | /// 31 | /// Gets the image data as a stream 32 | /// 33 | /// The result from compress 34 | /// Stream of compressed image data 35 | public static async Task GetImageStreamData(this Task result) where T : TinyPngImageResponse 36 | { 37 | var imageResponse = await result; 38 | return await imageResponse.GetImageStreamData(); 39 | } 40 | 41 | /// 42 | /// Gets the image data as a stream 43 | /// 44 | /// The result from compress 45 | /// Stream of compressed image data 46 | public static async Task GetImageStreamData(this TinyPngImageResponse result) 47 | { 48 | return await result.HttpResponseMessage.Content.ReadAsStreamAsync(); 49 | } 50 | 51 | /// 52 | /// Writes the image to disk 53 | /// 54 | /// The result from compress 55 | /// The path to store the file 56 | /// 57 | public static async Task SaveImageToDisk(this Task result, string filePath) where T : TinyPngImageResponse 58 | { 59 | var response = await result; 60 | await SaveImageToDisk(response, filePath); 61 | } 62 | 63 | /// 64 | /// Writes the image to disk 65 | /// 66 | /// The result from compress 67 | /// The path to store the file 68 | /// 69 | public static async Task SaveImageToDisk(this TinyPngImageResponse result, string filePath) 70 | { 71 | var byteData = await result.GetImageByteData(); 72 | File.WriteAllBytes(filePath, byteData); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/TinyPNG/Extensions/ResizeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | using TinyPng.ResizeOperations; 6 | using TinyPng.Responses; 7 | 8 | namespace TinyPng; 9 | 10 | public static class ResizeExtensions 11 | { 12 | /// 13 | /// Uses the TinyPng API to create a resized version of your uploaded image. 14 | /// 15 | /// This is the previous result of running a compression 16 | /// Supply a strongly typed Resize Operation. See , , , 17 | /// 18 | public static async Task Resize(this Task result, ResizeOperation resizeOperation) 19 | { 20 | if (result == null) 21 | { 22 | throw new ArgumentNullException(nameof(result)); 23 | } 24 | 25 | if (resizeOperation == null) 26 | { 27 | throw new ArgumentNullException(nameof(resizeOperation)); 28 | } 29 | 30 | TinyPngCompressResponse compressResponse = await result; 31 | 32 | string requestBody = JsonSerializer.Serialize(new { resize = resizeOperation }, TinyPngClient._jsonOptions); 33 | 34 | HttpRequestMessage msg = new(HttpMethod.Post, compressResponse.Output.Url) 35 | { 36 | Content = new StringContent(requestBody, System.Text.Encoding.UTF8, "application/json") 37 | }; 38 | 39 | HttpResponseMessage response = await compressResponse._httpClient.SendAsync(msg); 40 | if (response.IsSuccessStatusCode) 41 | { 42 | return new TinyPngResizeResponse(response); 43 | } 44 | 45 | ApiErrorResponse errorMsg = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), TinyPngClient._jsonOptions); 46 | throw new TinyPngApiException((int)response.StatusCode, response.ReasonPhrase, errorMsg.Error, errorMsg.Message); 47 | } 48 | 49 | /// 50 | /// Uses the TinyPng API to create a resized version of your uploaded image. 51 | /// 52 | /// This is the previous result of running a compression 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | public static async Task Resize(this Task result, int width, int height, ResizeType resizeType = ResizeType.Fit) 60 | { 61 | if (result == null) 62 | { 63 | throw new ArgumentNullException(nameof(result)); 64 | } 65 | 66 | if (width == 0) 67 | { 68 | throw new ArgumentOutOfRangeException(nameof(width), "Width cannot be 0"); 69 | } 70 | 71 | if (height == 0) 72 | { 73 | throw new ArgumentOutOfRangeException(nameof(height), "Height cannot be 0"); 74 | } 75 | 76 | ResizeOperation resizeOp = new(resizeType, width, height); 77 | 78 | return await result.Resize(resizeOp); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/TinyPNG/PreserveMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TinyPng 4 | { 5 | [Flags] 6 | public enum PreserveMetadata 7 | { 8 | None = 1 << 0, 9 | Copyright = 1 << 1, 10 | Creation = 1 << 2, 11 | Location = 1 << 3 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/TinyPNG/ResizeOperations/CoverResizeOperation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TinyPng.ResizeOperations; 4 | 5 | public class CoverResizeOperation : ResizeOperation 6 | { 7 | public CoverResizeOperation(int width, int height) : base(ResizeType.Cover, width, height) 8 | { 9 | if (width == 0) 10 | { 11 | throw new ArgumentException("You must specify a width", nameof(width)); 12 | } 13 | if (height == 0) 14 | { 15 | throw new ArgumentException("You must specify a height", nameof(width)); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/TinyPNG/ResizeOperations/FitResizeOperation.cs: -------------------------------------------------------------------------------- 1 | namespace TinyPng.ResizeOperations; 2 | 3 | public class FitResizeOperation(int width, int height) : ResizeOperation(ResizeType.Fit, width, height) 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/TinyPNG/ResizeOperations/ResizeOperation.cs: -------------------------------------------------------------------------------- 1 | namespace TinyPng.ResizeOperations; 2 | 3 | public class ResizeOperation 4 | { 5 | public ResizeOperation(ResizeType type, int width, int height) 6 | { 7 | Method = type; 8 | Width = width; 9 | Height = height; 10 | } 11 | 12 | internal ResizeOperation(ResizeType type, int? width, int? height) 13 | { 14 | Method = type; 15 | Width = width; 16 | Height = height; 17 | } 18 | 19 | public int? Width { get; } 20 | public int? Height { get; } 21 | public ResizeType Method { get; } 22 | } 23 | 24 | public enum ResizeType 25 | { 26 | Fit, 27 | Scale, 28 | Cover 29 | } 30 | -------------------------------------------------------------------------------- /src/TinyPNG/ResizeOperations/ScaleHeightResizeOperation.cs: -------------------------------------------------------------------------------- 1 | namespace TinyPng.ResizeOperations; 2 | 3 | public class ScaleHeightResizeOperation(int height) : ResizeOperation(ResizeType.Scale, null, height) 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/TinyPNG/ResizeOperations/ScaleWidthResizeOperation.cs: -------------------------------------------------------------------------------- 1 | namespace TinyPng.ResizeOperations; 2 | 3 | public class ScaleWidthResizeOperation(int width) : ResizeOperation(ResizeType.Scale, width, null) 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/TinyPNG/Responses/ApiErrorResponse.cs: -------------------------------------------------------------------------------- 1 | namespace TinyPng.Responses; 2 | 3 | public class ApiErrorResponse 4 | { 5 | public string Error { get; set; } 6 | public string Message { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/TinyPNG/Responses/TinyPngCompressResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | 5 | namespace TinyPng.Responses; 6 | 7 | public class TinyPngCompressResponse : TinyPngResponse 8 | { 9 | public TinyPngApiInput Input { get; private set; } 10 | public TinyPngApiOutput Output { get; private set; } 11 | public TinyPngApiResult ApiResult { get; private set; } 12 | 13 | internal readonly HttpClient _httpClient; 14 | 15 | public TinyPngCompressResponse(HttpResponseMessage msg, HttpClient httpClient) : base(msg) 16 | { 17 | _httpClient = httpClient; 18 | 19 | //this is a cute trick to handle async in a ctor and avoid deadlocks 20 | ApiResult = Task.Run(() => Deserialize(msg)).GetAwaiter().GetResult(); 21 | Input = ApiResult.Input; 22 | Output = ApiResult.Output; 23 | 24 | } 25 | private async Task Deserialize(HttpResponseMessage response) 26 | { 27 | return await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), TinyPngClient._jsonOptions); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/TinyPNG/Responses/TinyPngConvertResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.Http; 3 | 4 | namespace TinyPng.Responses; 5 | public class TinyPngConvertResponse(HttpResponseMessage msg) : TinyPngImageResponse(msg) 6 | { 7 | public string ContentType => HttpResponseMessage.Content.Headers.ContentType.MediaType; 8 | public string ImageHeight => HttpResponseMessage.Content.Headers.GetValues("Image-Height").FirstOrDefault(); 9 | public string ImageWidth => HttpResponseMessage.Content.Headers.GetValues("Image-Width").FirstOrDefault(); 10 | } 11 | -------------------------------------------------------------------------------- /src/TinyPNG/Responses/TinyPngImageResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace TinyPng.Responses; 4 | 5 | /// 6 | /// This is a response which contains actual image data 7 | /// 8 | public class TinyPngImageResponse(HttpResponseMessage msg) : TinyPngResponse(msg) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/TinyPNG/Responses/TinyPngResizeResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace TinyPng.Responses; 4 | 5 | public class TinyPngResizeResponse(HttpResponseMessage msg) : TinyPngImageResponse(msg) 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/TinyPNG/Responses/TinyPngResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net.Http; 4 | 5 | namespace TinyPng.Responses; 6 | 7 | public class TinyPngResponse 8 | { 9 | internal HttpResponseMessage HttpResponseMessage { get; } 10 | 11 | private readonly int _compressionCount; 12 | 13 | public int CompressionCount => _compressionCount; 14 | 15 | 16 | protected TinyPngResponse(HttpResponseMessage msg) 17 | { 18 | if (msg.Headers.TryGetValues("Compression-Count", out IEnumerable compressionCountHeaders)) 19 | { 20 | int.TryParse(compressionCountHeaders.First(), out _compressionCount); 21 | } 22 | HttpResponseMessage = msg; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/TinyPNG/TinyPNG.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Copyright Chad Tolkien & Contributors 5 | TinyPNG 6 | 4.0.1 7 | Chad Tolkien 8 | latest 9 | netstandard2.0 10 | TinyPNG 11 | TinyPNG 12 | TinyPng 13 | tinypng;images;compression;jpg;png;webp 14 | icon.png 15 | This is a .NET Standard wrapper around the http://tinypng.com image compression service. 16 | https://github.com/ctolkien/TinyPNG 17 | MIT 18 | 19 | * 4.0 - Moved to System.Text.Json. Removed Newtonsoft Json. Added support for converting image file types 20 | * 3.3 - Support for netstandard 2.0. Added compress from URL feature thanks to @d-ugarov 21 | * 3.1 - Fixed bug to do with disposed HttpClient 22 | 23 | git 24 | https://github.com/ctolkien/TinyPNG.git 25 | true 26 | true 27 | snupkg 28 | true 29 | 30 | 31 | 32 | 33 | 34 | true 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/TinyPNG/TinyPngApiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TinyPng; 4 | 5 | public class TinyPngApiException : Exception 6 | { 7 | public int StatusCode { get; } 8 | public string StatusReasonPhrase { get; } 9 | public string ErrorTitle { get; } 10 | public string ErrorMessage { get; } 11 | 12 | 13 | public TinyPngApiException(int statusCode, string statusReasonPhrase, string errorTitle, string errorMessage) 14 | { 15 | ErrorTitle = errorTitle; 16 | ErrorMessage = errorMessage; 17 | StatusCode = statusCode; 18 | StatusReasonPhrase = statusReasonPhrase; 19 | 20 | Data.Add(nameof(ErrorTitle), ErrorTitle); 21 | Data.Add(nameof(ErrorMessage), ErrorMessage); 22 | Data.Add(nameof(StatusCode), StatusCode); 23 | Data.Add(nameof(StatusReasonPhrase), StatusReasonPhrase); 24 | } 25 | 26 | public override string Message => 27 | $"Api Service returned a non-success status code when attempting an operation on an image: {StatusCode} - {StatusReasonPhrase}. {ErrorTitle}, {ErrorMessage}"; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/TinyPNG/TinyPngApiResult.cs: -------------------------------------------------------------------------------- 1 | namespace TinyPng; 2 | 3 | public class TinyPngApiResult 4 | { 5 | public TinyPngApiInput Input { get; set; } 6 | public TinyPngApiOutput Output { get; set; } 7 | } 8 | 9 | public class TinyPngApiInput 10 | { 11 | public int Size { get; set; } 12 | public string Type { get; set; } 13 | } 14 | 15 | public class TinyPngApiOutput 16 | { 17 | public int Size { get; set; } 18 | public string Type { get; set; } 19 | public int Width { get; set; } 20 | public int Height { get; set; } 21 | public float Ratio { get; set; } 22 | public string Url { get; set; } 23 | } 24 | -------------------------------------------------------------------------------- /src/TinyPNG/TinyPngClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Text.Json.Serialization; 9 | using System.Threading.Tasks; 10 | using TinyPng.Responses; 11 | 12 | 13 | [assembly: InternalsVisibleTo("TinyPng.Tests")] 14 | namespace TinyPng; 15 | 16 | public class TinyPngClient 17 | { 18 | private const string _apiEndpoint = "https://api.tinify.com/shrink"; 19 | 20 | private readonly HttpClient _httpClient; 21 | internal static readonly JsonSerializerOptions _jsonOptions; 22 | 23 | /// 24 | /// Configures the client to use these AmazonS3 settings when storing images in S3 25 | /// 26 | public AmazonS3Configuration AmazonS3Configuration { get; set; } 27 | 28 | static TinyPngClient() 29 | { 30 | //configure json settings for camelCase. 31 | _jsonOptions = new JsonSerializerOptions 32 | { 33 | PropertyNameCaseInsensitive = true, 34 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 35 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 36 | }; 37 | 38 | _jsonOptions.Converters.Add(new CustomJsonStringEnumConverter(JsonNamingPolicy.CamelCase)); 39 | 40 | } 41 | 42 | /// 43 | /// Wrapper for the tinypng.com API 44 | /// 45 | /// Your tinypng.com API key, signup here: https://tinypng.com/developers 46 | /// HttpClient for requests (optional) 47 | public TinyPngClient(string apiKey, HttpClient httpClient = null) 48 | { 49 | if (string.IsNullOrEmpty(apiKey)) 50 | throw new ArgumentNullException(nameof(apiKey)); 51 | 52 | _httpClient = httpClient ?? new HttpClient(); 53 | 54 | ConfigureHttpClient(apiKey); 55 | } 56 | 57 | private void ConfigureHttpClient(string apiKey) 58 | { 59 | //configure basic auth api key formatting. 60 | var auth = $"api:{apiKey}"; 61 | var authByteArray = Encoding.ASCII.GetBytes(auth); 62 | var apiKeyEncoded = Convert.ToBase64String(authByteArray); 63 | 64 | //add auth to the default outgoing headers. 65 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", apiKeyEncoded); 66 | } 67 | 68 | /// 69 | /// Wrapper for the tinypng.com API 70 | /// 71 | /// Your tinypng.com API key, signup here: https://tinypng.com/developers 72 | /// Configures defaults to use for storing images on Amazon S3 73 | /// HttpClient for requests (optional) 74 | public TinyPngClient(string apiKey, AmazonS3Configuration amazonConfiguration, HttpClient httpClient = null) 75 | : this(apiKey, httpClient) 76 | { 77 | if (string.IsNullOrEmpty(apiKey)) 78 | throw new ArgumentNullException(nameof(apiKey)); 79 | 80 | AmazonS3Configuration = amazonConfiguration ?? throw new ArgumentNullException(nameof(amazonConfiguration)); 81 | } 82 | 83 | /// 84 | /// Compress a file on disk 85 | /// 86 | /// Path to file on disk 87 | /// TinyPngApiResult, 88 | public async Task Compress(string pathToFile) 89 | { 90 | if (string.IsNullOrEmpty(pathToFile)) 91 | throw new ArgumentNullException(nameof(pathToFile)); 92 | 93 | using var file = File.OpenRead(pathToFile); 94 | return await Compress(file).ConfigureAwait(false); 95 | } 96 | 97 | /// 98 | /// Compress byte array of image 99 | /// 100 | /// Byte array of the data to compress 101 | /// TinyPngApiResult, 102 | public async Task Compress(byte[] data) 103 | { 104 | if (data == null) 105 | throw new ArgumentNullException(nameof(data)); 106 | 107 | using var stream = new MemoryStream(data); 108 | return await Compress(stream).ConfigureAwait(false); 109 | } 110 | 111 | /// 112 | /// Compress a stream 113 | /// 114 | /// TinyPngApiResult, 115 | public Task Compress(Stream data) 116 | { 117 | if (data == null) 118 | throw new ArgumentNullException(nameof(data)); 119 | 120 | return CompressInternal(new StreamContent(data)); 121 | } 122 | 123 | /// 124 | /// Compress image from url 125 | /// 126 | /// Image url to compress 127 | /// TinyPngApiResult, 128 | public Task Compress(Uri url) 129 | { 130 | if (url is null) 131 | throw new ArgumentNullException(nameof(url)); 132 | 133 | return CompressInternal(CreateContent(url)); 134 | 135 | static HttpContent CreateContent(Uri source) => new StringContent( 136 | JsonSerializer.Serialize(new { source = new { url = source } }, _jsonOptions), 137 | Encoding.UTF8, "application/json"); 138 | } 139 | 140 | private async Task CompressInternal(HttpContent contentData) 141 | { 142 | var response = await _httpClient.PostAsync(_apiEndpoint, contentData).ConfigureAwait(false); 143 | 144 | if (response.IsSuccessStatusCode) 145 | return new TinyPngCompressResponse(response, _httpClient); 146 | 147 | var errorMsg = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); 148 | throw new TinyPngApiException((int)response.StatusCode, response.ReasonPhrase, errorMsg.Error, errorMsg.Message); 149 | } 150 | 151 | /// 152 | /// Stores a previously compressed image directly into Amazon S3 storage 153 | /// 154 | /// The previously compressed image 155 | /// The settings for the amazon connection 156 | /// The path and bucket to store in: bucket/file.png format 157 | /// 158 | public async Task SaveCompressedImageToAmazonS3(TinyPngCompressResponse result, AmazonS3Configuration amazonSettings, string path) 159 | { 160 | if (result == null) 161 | throw new ArgumentNullException(nameof(result)); 162 | if (amazonSettings == null) 163 | throw new ArgumentNullException(nameof(amazonSettings)); 164 | if (string.IsNullOrEmpty(path)) 165 | throw new ArgumentNullException(nameof(path)); 166 | 167 | amazonSettings.Path = path; 168 | 169 | var amazonSettingsAsJson = JsonSerializer.Serialize(new { store = amazonSettings }, _jsonOptions); 170 | 171 | var msg = new HttpRequestMessage(HttpMethod.Post, result.Output.Url) 172 | { 173 | Content = new StringContent(amazonSettingsAsJson, System.Text.Encoding.UTF8, "application/json") 174 | }; 175 | var response = await _httpClient.SendAsync(msg).ConfigureAwait(false); 176 | 177 | if (response.IsSuccessStatusCode) 178 | { 179 | return response.Headers.Location; 180 | } 181 | 182 | var errorMsg = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); 183 | throw new TinyPngApiException((int)response.StatusCode, response.ReasonPhrase, errorMsg.Error, errorMsg.Message); 184 | } 185 | 186 | /// 187 | /// Stores a previously compressed image directly into Amazon S3 storage 188 | /// 189 | /// The previously compressed image 190 | /// The path to storage the image as 191 | /// Optional: To override the previously configured bucket 192 | /// Optional: To override the previously configured region 193 | /// 194 | public Task SaveCompressedImageToAmazonS3(TinyPngCompressResponse result, string path, string bucketOverride = "", string regionOverride = "") 195 | { 196 | if (result == null) 197 | throw new ArgumentNullException(nameof(result)); 198 | if (AmazonS3Configuration == null) 199 | throw new InvalidOperationException("AmazonS3Configuration has not been configured"); 200 | if (string.IsNullOrEmpty(path)) 201 | throw new ArgumentNullException(nameof(path)); 202 | 203 | var amazonSettings = AmazonS3Configuration.Clone(); 204 | amazonSettings.Path = path; 205 | 206 | if (!string.IsNullOrEmpty(regionOverride)) 207 | amazonSettings.Region = regionOverride; 208 | 209 | if (!string.IsNullOrEmpty(bucketOverride)) 210 | amazonSettings.Bucket = bucketOverride; 211 | 212 | return SaveCompressedImageToAmazonS3(result, amazonSettings, path); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/TinyPng.Tests/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Text.Json; 5 | 6 | namespace TinyPng.Tests; 7 | 8 | internal static class Extensions 9 | { 10 | public static FakeResponseHandler Compress(this FakeResponseHandler fakeResponse) 11 | { 12 | TinyPngApiResult content = new() 13 | { 14 | Input = new TinyPngApiInput 15 | { 16 | Size = 18031, 17 | Type = "image/jpeg" 18 | }, 19 | Output = new TinyPngApiOutput 20 | { 21 | Width = 400, 22 | Height = 400, 23 | Size = 16646, 24 | Type = "image/jpeg", 25 | Ratio = 0.9232f, 26 | Url = "https://api.tinify.com/output" 27 | } 28 | }; 29 | HttpResponseMessage compressResponseMessage = new() 30 | { 31 | StatusCode = System.Net.HttpStatusCode.Created, 32 | Content = new StringContent(JsonSerializer.Serialize(content)), 33 | }; 34 | compressResponseMessage.Headers.Location = new Uri("https://api.tinify.com/output"); 35 | compressResponseMessage.Headers.Add("Compression-Count", "99"); 36 | 37 | fakeResponse.AddFakePostResponse(new Uri("https://api.tinify.com/shrink"), compressResponseMessage); 38 | return fakeResponse; 39 | } 40 | 41 | public static FakeResponseHandler CompressAndFail(this FakeResponseHandler fakeResponse) 42 | { 43 | TinyPngApiException errorApiObject = new(400, "reason", "title", "message"); 44 | 45 | HttpResponseMessage compressResponseMessage = new() 46 | { 47 | StatusCode = System.Net.HttpStatusCode.BadRequest, 48 | Content = new StringContent(JsonSerializer.Serialize(errorApiObject)) 49 | }; 50 | fakeResponse.AddFakePostResponse(new Uri("https://api.tinify.com/shrink"), compressResponseMessage); 51 | return fakeResponse; 52 | } 53 | 54 | public static FakeResponseHandler Download(this FakeResponseHandler fakeResponse) 55 | { 56 | FileStream compressedCatStream = File.OpenRead(TinyPngTests._compressedCat); 57 | HttpResponseMessage outputResponseMessage = new() 58 | { 59 | Content = new StreamContent(compressedCatStream), 60 | StatusCode = System.Net.HttpStatusCode.OK 61 | }; 62 | 63 | fakeResponse.AddFakeGetResponse(new Uri("https://api.tinify.com/output"), outputResponseMessage); 64 | return fakeResponse; 65 | } 66 | 67 | public static FakeResponseHandler DownloadAndFail(this FakeResponseHandler fakeResponse) 68 | { 69 | HttpResponseMessage outputResponseMessage = new() 70 | { 71 | Content = new StringContent(JsonSerializer.Serialize(new Responses.ApiErrorResponse { Error = "Stuff's on fire yo!", Message = "This is the error message" })), 72 | StatusCode = System.Net.HttpStatusCode.InternalServerError 73 | }; 74 | 75 | fakeResponse.AddFakeGetResponse(new Uri("https://api.tinify.com/output"), outputResponseMessage); 76 | return fakeResponse; 77 | } 78 | 79 | public static FakeResponseHandler Resize(this FakeResponseHandler fakeResponse) 80 | { 81 | FileStream resizedCatStream = File.OpenRead(TinyPngTests._resizedCat); 82 | HttpResponseMessage resizeMessage = new() 83 | { 84 | StatusCode = System.Net.HttpStatusCode.OK, 85 | Content = new StreamContent(resizedCatStream) 86 | }; 87 | resizeMessage.Headers.Add("Image-Width", "150"); 88 | resizeMessage.Headers.Add("Image-Height", "150"); 89 | 90 | fakeResponse.AddFakePostResponse(new Uri("https://api.tinify.com/output"), resizeMessage); 91 | return fakeResponse; 92 | } 93 | 94 | public static FakeResponseHandler S3(this FakeResponseHandler fakeResponse) 95 | { 96 | HttpResponseMessage amazonMessage = new() 97 | { 98 | StatusCode = System.Net.HttpStatusCode.OK 99 | }; 100 | amazonMessage.Headers.Add("Location", "https://s3-ap-southeast-2.amazonaws.com/tinypng-test-bucket/path.jpg"); 101 | 102 | fakeResponse.AddFakePostResponse(new Uri("https://api.tinify.com/output"), amazonMessage); 103 | return fakeResponse; 104 | } 105 | 106 | public static FakeResponseHandler S3AndFail(this FakeResponseHandler fakeResponse) 107 | { 108 | HttpResponseMessage amazonMessage = new() 109 | { 110 | Content = new StringContent(JsonSerializer.Serialize(new Responses.ApiErrorResponse { Error = "Stuff's on fire yo!", Message = "This is the error message" })), 111 | StatusCode = System.Net.HttpStatusCode.BadRequest 112 | }; 113 | //amazonMessage.Headers.Add("Location", "https://s3-ap-southeast-2.amazonaws.com/tinypng-test-bucket/path.jpg"); 114 | 115 | fakeResponse.AddFakePostResponse(new Uri("https://api.tinify.com/output"), amazonMessage); 116 | return fakeResponse; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/TinyPng.Tests/FakeResponseHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace TinyPng.Tests; 8 | 9 | public class FakeResponseHandler : DelegatingHandler 10 | { 11 | private readonly Dictionary _fakeGetResponses = []; 12 | private readonly Dictionary _fakePostResponses = []; 13 | 14 | 15 | public void AddFakeGetResponse(Uri uri, HttpResponseMessage responseMessage) 16 | { 17 | _fakeGetResponses.Add(uri, responseMessage); 18 | } 19 | public void AddFakePostResponse(Uri uri, HttpResponseMessage responseMessage) 20 | { 21 | _fakePostResponses.Add(uri, responseMessage); 22 | } 23 | 24 | protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) 25 | { 26 | if (request.Method == HttpMethod.Get && _fakeGetResponses.TryGetValue(request.RequestUri, out var getMessage)) { return Task.FromResult(getMessage); } 27 | else if (request.Method == HttpMethod.Post && _fakePostResponses.TryGetValue(request.RequestUri, out var postMessage)) { return Task.FromResult(postMessage); } 28 | else { return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) { RequestMessage = request }); } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/TinyPng.Tests/Resources/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctolkien/TinyPNG/3ebe83852ba0c45a224c531ce7da6b864cdfd229/tests/TinyPng.Tests/Resources/cat.jpg -------------------------------------------------------------------------------- /tests/TinyPng.Tests/Resources/compressedcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctolkien/TinyPNG/3ebe83852ba0c45a224c531ce7da6b864cdfd229/tests/TinyPng.Tests/Resources/compressedcat.jpg -------------------------------------------------------------------------------- /tests/TinyPng.Tests/Resources/resizedcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctolkien/TinyPNG/3ebe83852ba0c45a224c531ce7da6b864cdfd229/tests/TinyPng.Tests/Resources/resizedcat.jpg -------------------------------------------------------------------------------- /tests/TinyPng.Tests/TinyPNG.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | all 36 | runtime; build; native; contentfiles; analyzers; buildtransitive 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/TinyPng.Tests/TinyPngTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using TinyPng.ResizeOperations; 6 | using Xunit; 7 | 8 | namespace TinyPng.Tests; 9 | 10 | public class TinyPngTests 11 | { 12 | private const string _apiKey = "lolwat"; 13 | 14 | internal const string _cat = "Resources/cat.jpg"; 15 | internal const string _compressedCat = "Resources/compressedcat.jpg"; 16 | internal const string _resizedCat = "Resources/resizedcat.jpg"; 17 | internal const string _savedCatPath = "Resources/savedcat.jpg"; 18 | 19 | [Fact] 20 | public void TinyPngClientThrowsWhenNoApiKeySupplied() 21 | { 22 | _ = Assert.Throws(() => new TinyPngClient(null)); 23 | } 24 | 25 | [Fact] 26 | public void TinyPngClientThrowsWhenNoValidS3ConfigurationSupplied() 27 | { 28 | _ = Assert.Throws(() => new TinyPngClient(null)); 29 | _ = Assert.Throws(() => new TinyPngClient(null, (AmazonS3Configuration)null)); 30 | _ = Assert.Throws(() => new TinyPngClient("apiKey", (AmazonS3Configuration)null)); 31 | _ = Assert.Throws(() => new TinyPngClient(null, new AmazonS3Configuration("a", "b", "c", "d"))); 32 | } 33 | 34 | [Fact] 35 | public void HandleScenarioOfExistingAuthHeaderOnTheClient() 36 | { 37 | var httpClient = new HttpClient(); 38 | httpClient.DefaultRequestHeaders.Add("Authorization", "Basic dGVzdDp0ZXN0"); 39 | 40 | _ = new TinyPngClient("test", httpClient); 41 | 42 | //This just ensures that it doesn't throw 43 | } 44 | 45 | [Fact] 46 | public async Task Compression() 47 | { 48 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress())); 49 | 50 | Responses.TinyPngCompressResponse result = await pngx.Compress(_cat); 51 | 52 | Assert.Equal("image/jpeg", result.Input.Type); 53 | Assert.Equal(400, result.Output.Width); 54 | Assert.Equal(400, result.Output.Height); 55 | } 56 | 57 | [Fact] 58 | public async Task CanBeCalledMultipleTimesWithoutExploding() 59 | { 60 | TinyPngClient pngx1 = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress())); 61 | 62 | Responses.TinyPngCompressResponse result1 = await pngx1.Compress(_cat); 63 | 64 | Assert.Equal("image/jpeg", result1.Input.Type); 65 | Assert.Equal(400, result1.Output.Width); 66 | Assert.Equal(400, result1.Output.Height); 67 | 68 | 69 | TinyPngClient pngx2 = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress())); 70 | 71 | Responses.TinyPngCompressResponse result2 = await pngx2.Compress(_cat); 72 | 73 | Assert.Equal("image/jpeg", result2.Input.Type); 74 | Assert.Equal(400, result2.Output.Width); 75 | Assert.Equal(400, result2.Output.Height); 76 | } 77 | 78 | [Fact] 79 | public async Task CompressionCount() 80 | { 81 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress())); 82 | 83 | Responses.TinyPngCompressResponse result = await pngx.Compress(_cat); 84 | 85 | Assert.Equal(99, result.CompressionCount); 86 | } 87 | 88 | [Fact] 89 | public async Task CompressionWithBytes() 90 | { 91 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress())); 92 | 93 | byte[] bytes = await File.ReadAllBytesAsync(_cat); 94 | 95 | Responses.TinyPngCompressResponse result = await pngx.Compress(bytes); 96 | 97 | Assert.Equal("image/jpeg", result.Input.Type); 98 | Assert.Equal(400, result.Output.Width); 99 | Assert.Equal(400, result.Output.Height); 100 | } 101 | 102 | [Fact] 103 | public async Task CompressionWithStreams() 104 | { 105 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress())); 106 | 107 | await using FileStream fileStream = File.OpenRead(_cat); 108 | 109 | Responses.TinyPngCompressResponse result = await pngx.Compress(fileStream); 110 | 111 | Assert.Equal("image/jpeg", result.Input.Type); 112 | Assert.Equal(400, result.Output.Width); 113 | Assert.Equal(400, result.Output.Height); 114 | } 115 | 116 | [Fact] 117 | public async Task CompressionFromUrl() 118 | { 119 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress())); 120 | 121 | Responses.TinyPngCompressResponse result = await pngx.Compress(new Uri("https://sample.com/image.jpg")); 122 | 123 | Assert.Equal("image/jpeg", result.Input.Type); 124 | Assert.Equal(400, result.Output.Width); 125 | Assert.Equal(400, result.Output.Height); 126 | } 127 | 128 | [Fact] 129 | public async Task CompressionShouldThrowIfNoPathToFile() 130 | { 131 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress())); 132 | 133 | await Assert.ThrowsAsync(async () => await pngx.Compress(string.Empty)); 134 | } 135 | 136 | [Fact] 137 | public async Task CompressionShouldThrowIfNoNonSuccessStatusCode() 138 | { 139 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().CompressAndFail())); 140 | 141 | await Assert.ThrowsAsync(async () => await pngx.Compress(_cat)); 142 | } 143 | 144 | [Fact] 145 | public async Task CompressionAndDownload() 146 | { 147 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Download())); 148 | 149 | byte[] downloadResult = await pngx.Compress(_cat) 150 | .Download() 151 | .GetImageByteData(); 152 | 153 | Assert.Equal(16646, downloadResult.Length); 154 | } 155 | 156 | [Fact] 157 | public async Task CompressionAndDownloadAndGetUnderlyingStream() 158 | { 159 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Download())); 160 | 161 | Stream downloadResult = await pngx.Compress(_cat) 162 | .Download() 163 | .GetImageStreamData(); 164 | 165 | Assert.Equal(16646, downloadResult.Length); 166 | } 167 | 168 | [Fact] 169 | public async Task CompressionAndDownloadAndWriteToDisk() 170 | { 171 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Download())); 172 | try 173 | { 174 | await pngx.Compress(_cat) 175 | .Download() 176 | .SaveImageToDisk(_savedCatPath); 177 | } 178 | finally 179 | { 180 | //try cleanup any saved file 181 | File.Delete(_savedCatPath); 182 | } 183 | } 184 | 185 | [Fact] 186 | public async Task ResizingOperationThrows() 187 | { 188 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Resize())); 189 | 190 | await Assert.ThrowsAsync(async () => await pngx.Compress((string)null).Resize(150, 150)); 191 | await Assert.ThrowsAsync(async () => await pngx.Compress((string)null).Resize(null)); 192 | await Assert.ThrowsAsync(async () => await pngx.Compress(_cat).Resize(null)); 193 | 194 | Task nullCompressResponse = null; 195 | await Assert.ThrowsAsync(async () => await nullCompressResponse.Resize(150, 150)); 196 | 197 | await Assert.ThrowsAsync(async () => await pngx.Compress(_cat).Resize(0, 150)); 198 | await Assert.ThrowsAsync(async () => await pngx.Compress(_cat).Resize(150, 0)); 199 | } 200 | 201 | [Fact] 202 | public async Task DownloadingOperationThrows() 203 | { 204 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Download())); 205 | 206 | await Assert.ThrowsAsync(async () => await pngx.Compress((string)null).Download()); 207 | 208 | Task nullCompressResponse = null; 209 | await Assert.ThrowsAsync(async () => await nullCompressResponse.Download()); 210 | } 211 | 212 | [Fact] 213 | public async Task DownloadingOperationThrowsOnNonSuccessStatusCode() 214 | { 215 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().DownloadAndFail())); 216 | 217 | await Assert.ThrowsAsync(async () => await pngx.Compress(_cat).Download()); 218 | } 219 | 220 | [Fact] 221 | public async Task ResizingOperation() 222 | { 223 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Resize())); 224 | 225 | byte[] resizedImageByteData = await pngx.Compress(_cat).Resize(150, 150).GetImageByteData(); 226 | 227 | Assert.Equal(5970, resizedImageByteData.Length); 228 | } 229 | 230 | [Fact] 231 | public async Task ResizingScaleHeightOperation() 232 | { 233 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Resize())); 234 | 235 | byte[] resizedImageByteData = await pngx.Compress(_cat).Resize(new ScaleHeightResizeOperation(150)).GetImageByteData(); 236 | 237 | Assert.Equal(5970, resizedImageByteData.Length); 238 | } 239 | 240 | [Fact] 241 | public async Task ResizingScaleWidthOperation() 242 | { 243 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Resize())); 244 | 245 | byte[] resizedImageByteData = await pngx.Compress(_cat).Resize(new ScaleWidthResizeOperation(150)).GetImageByteData(); 246 | 247 | Assert.Equal(5970, resizedImageByteData.Length); 248 | } 249 | 250 | [Fact] 251 | public async Task ResizingFitResizeOperation() 252 | { 253 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Resize())); 254 | 255 | byte[] resizedImageByteData = await pngx.Compress(_cat).Resize(new FitResizeOperation(150, 150)).GetImageByteData(); 256 | 257 | Assert.Equal(5970, resizedImageByteData.Length); 258 | } 259 | 260 | [Fact] 261 | public async Task ResizingCoverResizeOperation() 262 | { 263 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Resize())); 264 | 265 | byte[] resizedImageByteData = await pngx.Compress(_cat).Resize(new CoverResizeOperation(150, 150)).GetImageByteData(); 266 | 267 | Assert.Equal(5970, resizedImageByteData.Length); 268 | } 269 | 270 | [Fact] 271 | public async Task ResizingCoverResizeOperationThrowsWithInvalidParams() 272 | { 273 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().Resize())); 274 | 275 | await Assert.ThrowsAsync(async () => await pngx.Compress(_cat).Resize(new CoverResizeOperation(0, 150))); 276 | await Assert.ThrowsAsync(async () => await pngx.Compress(_cat).Resize(new CoverResizeOperation(150, 0))); 277 | } 278 | 279 | [Fact] 280 | public void CompressAndStoreToS3ShouldThrowIfNoApiKeyProvided() 281 | { 282 | _ = Assert.Throws(() => new TinyPngClient(string.Empty, new AmazonS3Configuration("a", "b", "c", "d"))); 283 | } 284 | 285 | [Fact] 286 | public async Task CompressAndStoreToS3ShouldThrowIfS3HasNotBeenConfigured() 287 | { 288 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().S3())); 289 | 290 | Responses.TinyPngCompressResponse result = await pngx.Compress(_cat); 291 | 292 | await Assert.ThrowsAsync(async () => await pngx.SaveCompressedImageToAmazonS3(null, "bucket/path.jpg")); 293 | await Assert.ThrowsAsync(async () => await pngx.SaveCompressedImageToAmazonS3(result, string.Empty)); 294 | await Assert.ThrowsAsync(async () => await pngx.SaveCompressedImageToAmazonS3(result, "bucket/path.jpg")); 295 | } 296 | 297 | private const string _awsAccessKeyId = "lolwat"; 298 | private const string _awsSecretAccessKey = "lolwat"; 299 | 300 | [Fact] 301 | public async Task CompressAndStoreToS3() 302 | { 303 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().S3())); 304 | 305 | Responses.TinyPngCompressResponse result = await pngx.Compress(_cat); 306 | 307 | string sendToAmazon = (await pngx.SaveCompressedImageToAmazonS3(result, 308 | new AmazonS3Configuration(_awsAccessKeyId, _awsSecretAccessKey, "tinypng-test-bucket", "ap-southeast-2"), 309 | "path.jpg")).ToString(); 310 | 311 | Assert.Equal("https://s3-ap-southeast-2.amazonaws.com/tinypng-test-bucket/path.jpg", sendToAmazon); 312 | } 313 | 314 | [Fact] 315 | public async Task CompressAndStoreToS3FooBar() 316 | { 317 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().S3AndFail())); 318 | 319 | Responses.TinyPngCompressResponse result = await pngx.Compress(_cat); 320 | 321 | await Assert.ThrowsAsync(async () => 322 | await pngx.SaveCompressedImageToAmazonS3(result, 323 | new AmazonS3Configuration(_awsAccessKeyId, _awsSecretAccessKey, "tinypng-test-bucket", "ap-southeast-2"), "path")); 324 | } 325 | 326 | [Fact] 327 | public async Task CompressAndStoreToS3Throws() 328 | { 329 | TinyPngClient pngx = new(_apiKey, new HttpClient(new FakeResponseHandler().Compress().S3())); 330 | 331 | Responses.TinyPngCompressResponse result = await pngx.Compress(_cat); 332 | 333 | _ = await Assert.ThrowsAsync(async () => await pngx.SaveCompressedImageToAmazonS3(result, null, string.Empty)); 334 | 335 | //S3 configuration has not been set 336 | _ = await Assert.ThrowsAsync(async () => await pngx.SaveCompressedImageToAmazonS3(result, path: string.Empty)); 337 | } 338 | 339 | [Fact] 340 | public async Task CompressAndStoreToS3WithOptionsPassedIntoConstructor() 341 | { 342 | TinyPngClient pngx = new(_apiKey, 343 | new AmazonS3Configuration(_awsAccessKeyId, _awsSecretAccessKey, "tinypng-test-bucket", "ap-southeast-2"), 344 | new HttpClient(new FakeResponseHandler().Compress().S3())); 345 | 346 | Responses.TinyPngCompressResponse result = await pngx.Compress(_cat); 347 | string sendToAmazon = (await pngx.SaveCompressedImageToAmazonS3(result, "path.jpg")).ToString(); 348 | 349 | Assert.Equal("https://s3-ap-southeast-2.amazonaws.com/tinypng-test-bucket/path.jpg", sendToAmazon); 350 | } 351 | 352 | [Fact] 353 | public void TinyPngExceptionPopulatesCorrectData() 354 | { 355 | int StatusCode = 200; 356 | string StatusReasonPhrase = "status"; 357 | string ErrorTitle = "title"; 358 | string ErrorMessage = "message"; 359 | TinyPngApiException e = new(StatusCode, StatusReasonPhrase, ErrorTitle, "message"); 360 | 361 | string msg = $"Api Service returned a non-success status code when attempting an operation on an image: {StatusCode} - {StatusReasonPhrase}. {ErrorTitle}, {ErrorMessage}"; 362 | 363 | Assert.Equal(StatusCode, e.StatusCode); 364 | Assert.Equal(StatusReasonPhrase, e.StatusReasonPhrase); 365 | Assert.Equal(ErrorTitle, e.ErrorTitle); 366 | Assert.Equal(msg, e.Message); 367 | } 368 | } 369 | --------------------------------------------------------------------------------