├── .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 | [](https://www.nuget.org/packages/TinyPNG)
4 | [](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 |
--------------------------------------------------------------------------------