├── .gitattributes ├── .gitignore ├── License.txt ├── PSP format specs ├── PSP File Format Specification 10.pdf ├── psp5spec.pdf ├── psp6spec.doc ├── psp7spec.doc └── psp8spec.doc ├── Readme.md ├── Test images ├── Flag Before.pspimage ├── Marble head.PspImage ├── Sunset_PSP10-CreateInfo.pspimage ├── Sunset_PSP10.pspimage ├── Sunset_PSP11.pspimage ├── Sunset_PSP12.pspimage ├── Sunset_PSP9.pspimage ├── Sunset_X3.pspimage ├── Sunset_X6.pspimage ├── psp9.pspimage └── th_goldhill.pspimage └── src ├── .editorconfig ├── CompressionFormats.cs ├── FileVersion.cs ├── FodyWeavers.xml ├── IO ├── BinaryWriterEx.cs ├── BlockLengthWriter.cs ├── EndianBinaryReader.cs ├── Endianess.cs └── StringReadOptions.cs ├── IgnoredWords.dic ├── PSPFileType.cs ├── PSPFileTypeFactory.cs ├── PSPSections ├── ChannelSubBlock.cs ├── ColorPaletteBlock.cs ├── CompositeImageBlock.cs ├── CreatorBlock.DebugView.cs ├── CreatorBlock.cs ├── CreatorBlockSerialization.cs ├── CreatorBlockSerializedData.cs ├── ExtendedDataBlock.cs ├── FileHeader.cs ├── GeneralImageAttributes.cs ├── LayerBlock.cs ├── PSPConstants.cs ├── PSPEnums.cs ├── PSPFile.cs ├── PSPUtil.cs ├── RGBQUAD.cs ├── RLE.cs └── ThumbnailBlock.cs ├── PaintShopProFiletype.csproj ├── PaintShopProFiletype.sln ├── PluginSupportInfo.cs └── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs └── Resources.resx /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | 290 | # Local repository 291 | 292 | FodyWeavers.xsd 293 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 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 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////// 24 | 25 | Portions of the software has been adapted from Photoshop PSD FileType Plugin for Paint.NET: 26 | http://psdplugin.codeplex.com/ 27 | 28 | These portions are provided under the MIT License: 29 | http://opensource.org/licenses/MIT 30 | 31 | ---- 32 | 33 | Copyright (c) 2006-2007 Frank Blumenberg 34 | Copyright (c) 2010-2013 Tao Yue 35 | 36 | MIT License: http://www.opensource.org/licenses/mit-license.php 37 | 38 | Permission is hereby granted, free of charge, to any person obtaining a copy 39 | of this software and associated documentation files (the "Software"), to deal 40 | in the Software without restriction, including without limitation the rights 41 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 42 | copies of the Software, and to permit persons to whom the Software is 43 | furnished to do so, subject to the following conditions: 44 | 45 | The above copyright notice and this permission notice shall be included in all 46 | copies or substantial portions of the Software. 47 | 48 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 49 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 50 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 51 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 52 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 53 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 54 | THE SOFTWARE. 55 | 56 | 57 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////// 58 | 59 | Portions of this software has been adapted from Paint.NET 3.36 60 | http://www.getpaint.net 61 | 62 | These portions are provided under the following license: 63 | 64 | Paint.NET 65 | Copyright (C) dotPDN LLC, Rick Brewster, Chris Crosetto, Tom Jackson, Michael Kelsey, Brandon Ortiz, Craig Taylor, Chris Trevino, and Luke Walker. 66 | Portions Copyright (C) Microsoft Corporation. All Rights Reserved. 67 | 68 | This software is licensed as per the MIT License below, but with three (3) exceptions: 69 | 70 | * Exception 1: The Paint.NET logo and icon artwork are Copyright (C) Rick Brewster. They are covered by the Creative Commons Attribution-NonCommercial-NoDerivs 2.5 license which is detailed here: http://creativecommons.org/licenses/by-nc-nd/2.5/ . However, permission is granted to use the logo and icon artwork in ways that directly discuss or promote Paint.NET (e.g. blog and news posts about Paint.NET, "Made with Paint.NET" watermarks or insets). 71 | 72 | * Exception 2: Paint.NET makes use of certain text and graphic resources that it comes with (e.g., toolbar icon graphics, text for menu items and the status bar). These are collectively referred to as "resource assets" and are defined to include the contents of files installed by Paint.NET, or included in its source code distribution, that have a .RESOURCES, .RESX, or .PNG file extension. This also includes embedded resource files within the PaintDotNet.Resources.dll installed file. These "resource assets" are covered by the Creative Commons Attribution-NonCommercial-NoDerivs 2.5 license which is detailed here: http://creativecommons.org/licenses/by-nc-nd/2.5/ . However, permission is granted to create and distribute derivative works of the "resource assets" for the sole purpose of providing a translation to a language other than English. Some "resource assets" are included in unmodified form from external icon or image libraries and are still covered by their original, respective licenses (e.g., "Silk", "Visual Studio 2005 Image Library"). 73 | 74 | * Exception 3: Although the Paint.NET source code distribution includes the GPC source code, use of the GPC code in any other commercial application is not permitted without a GPC Commercial Use Licence from The University of Manchester. For more information, please refer to the GPC website at: http://www.cs.man.ac.uk/~toby/alan/software/ 75 | 76 | MIT License: http://www.opensource.org/licenses/mit-license.php 77 | 78 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 81 | 82 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 83 | 84 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////// 85 | 86 | .NET Community Toolkit 87 | https://github.com/CommunityToolkit/dotnet 88 | 89 | ---- 90 | 91 | Copyright © .NET Foundation and Contributors 92 | 93 | All rights reserved. 94 | MIT License (MIT) 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 97 | 98 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 99 | 100 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /PSP format specs/PSP File Format Specification 10.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/PSP format specs/PSP File Format Specification 10.pdf -------------------------------------------------------------------------------- /PSP format specs/psp5spec.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/PSP format specs/psp5spec.pdf -------------------------------------------------------------------------------- /PSP format specs/psp6spec.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/PSP format specs/psp6spec.doc -------------------------------------------------------------------------------- /PSP format specs/psp7spec.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/PSP format specs/psp7spec.doc -------------------------------------------------------------------------------- /PSP format specs/psp8spec.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/PSP format specs/psp8spec.doc -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # pdn-pspformat 2 | 3 | A [Paint.NET](http://www.getpaint.net) FileType plugin that loads and saves Paint Shop Pro® images. 4 | 5 | ## How to install the plugin 6 | 7 | 1. Close Paint.NET. 8 | 2. Place PaintShopProFiletype.dll in the Paint.NET FileTypes folder which is usually located in one the following locations depending on the Paint.NET version you have installed. 9 | 10 | Paint.NET Version | FileTypes Folder Location 11 | --------|---------- 12 | Classic | C:\Program Files\Paint.NET\FileTypes 13 | Microsoft Store | Documents\paint.net App Files\FileTypes 14 | 15 | 3. Restart Paint.NET. 16 | 17 | ## License 18 | 19 | This project is licensed under the terms of the MIT License. 20 | See [License.txt](License.txt) for more information. -------------------------------------------------------------------------------- /Test images/Flag Before.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Flag Before.pspimage -------------------------------------------------------------------------------- /Test images/Marble head.PspImage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Marble head.PspImage -------------------------------------------------------------------------------- /Test images/Sunset_PSP10-CreateInfo.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Sunset_PSP10-CreateInfo.pspimage -------------------------------------------------------------------------------- /Test images/Sunset_PSP10.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Sunset_PSP10.pspimage -------------------------------------------------------------------------------- /Test images/Sunset_PSP11.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Sunset_PSP11.pspimage -------------------------------------------------------------------------------- /Test images/Sunset_PSP12.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Sunset_PSP12.pspimage -------------------------------------------------------------------------------- /Test images/Sunset_PSP9.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Sunset_PSP9.pspimage -------------------------------------------------------------------------------- /Test images/Sunset_X3.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Sunset_X3.pspimage -------------------------------------------------------------------------------- /Test images/Sunset_X6.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/Sunset_X6.pspimage -------------------------------------------------------------------------------- /Test images/psp9.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/psp9.pspimage -------------------------------------------------------------------------------- /Test images/th_goldhill.pspimage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xC0000054/pdn-pspformat/c0f6bbd8dee0e432f4332e5ba35eea817226fe62/Test images/th_goldhill.pspimage -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = crlf 8 | 9 | vsspell_section_id = 30a86b75973f41d3b6fe30b8f7950859 10 | vsspell_ignored_words_30a86b75973f41d3b6fe30b8f7950859 = File:IgnoredWords.dic 11 | 12 | [*.cs] 13 | indent_style = space 14 | indent_size = 4 15 | trim_trailing_whitespace = true 16 | 17 | # IDE0090: Simplify 'new' expression 18 | csharp_style_implicit_object_creation_when_type_is_apparent = false 19 | 20 | # IDE0057: Use range operator 21 | csharp_style_prefer_range_operator = false 22 | 23 | # Newline settings 24 | csharp_new_line_before_open_brace = all 25 | csharp_new_line_before_else = true 26 | csharp_new_line_before_catch = true 27 | csharp_new_line_before_finally = true 28 | csharp_new_line_before_members_in_object_initializers = true 29 | csharp_new_line_before_members_in_anonymous_types = true 30 | csharp_new_line_between_query_expression_clauses = true 31 | 32 | # Expression-bodied member settings 33 | csharp_style_expression_bodied_methods = false : silent 34 | csharp_style_expression_bodied_constructors = false : silent 35 | csharp_style_expression_bodied_operators = false : silent 36 | csharp_style_expression_bodied_properties = true : silent 37 | csharp_style_expression_bodied_indexers = true : silent 38 | csharp_style_expression_bodied_accessors = true : silent 39 | 40 | # Code block settings 41 | csharp_prefer_braces = true : warning 42 | 43 | # "This." qualifier settings 44 | dotnet_style_qualification_for_field = true : suggestion 45 | dotnet_style_qualification_for_property = true : suggestion 46 | dotnet_style_qualification_for_method = false : suggestion 47 | dotnet_style_qualification_for_event = true : suggestion 48 | -------------------------------------------------------------------------------- /src/CompressionFormats.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | namespace PaintShopProFiletype 13 | { 14 | internal enum CompressionFormats 15 | { 16 | None, 17 | LZ77 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/FileVersion.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | namespace PaintShopProFiletype 13 | { 14 | internal enum FileVersion 15 | { 16 | Version5 = 3, 17 | Version6, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /src/IO/BinaryWriterEx.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System.IO; 13 | 14 | namespace PaintShopProFiletype.IO 15 | { 16 | internal class BinaryWriterEx : BinaryWriter 17 | { 18 | private readonly bool ownStream; 19 | 20 | public BinaryWriterEx(Stream stream, bool ownStream) : base(stream) 21 | { 22 | this.ownStream = ownStream; 23 | } 24 | 25 | protected override void Dispose(bool disposing) 26 | { 27 | if (disposing && this.ownStream) 28 | { 29 | base.OutStream.Dispose(); 30 | } 31 | } 32 | 33 | public void Write(System.Drawing.Rectangle rect) 34 | { 35 | Write(rect.Left); 36 | Write(rect.Top); 37 | Write(rect.Right); 38 | Write(rect.Bottom); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/IO/BlockLengthWriter.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | // Portions of this file has been adapted from: 13 | ///////////////////////////////////////////////////////////////////////////////// 14 | // 15 | // Photoshop PSD FileType Plugin for Paint.NET 16 | // http://psdplugin.codeplex.com/ 17 | // 18 | // This software is provided under the MIT License: 19 | // Copyright (c) 2006-2007 Frank Blumenberg 20 | // Copyright (c) 2010-2013 Tao Yue 21 | // 22 | // See LICENSE.txt for complete licensing and attribution information. 23 | // 24 | ///////////////////////////////////////////////////////////////////////////////// 25 | 26 | using System; 27 | using System.IO; 28 | 29 | namespace PaintShopProFiletype.IO 30 | { 31 | internal class BlockLengthWriter : IDisposable 32 | { 33 | private readonly BinaryWriter writer; 34 | private readonly long startPos; 35 | private readonly long lengthPos; 36 | private bool disposed; 37 | 38 | public BlockLengthWriter(BinaryWriter binaryWriter) 39 | { 40 | this.writer = binaryWriter; 41 | this.lengthPos = this.writer.BaseStream.Position; 42 | this.writer.Write(0xBADF00DU); 43 | this.startPos = this.writer.BaseStream.Position; 44 | this.disposed = false; 45 | } 46 | 47 | public void Dispose() 48 | { 49 | if (!this.disposed) 50 | { 51 | long end = this.writer.BaseStream.Position; 52 | long sectionLength = end - this.startPos; 53 | 54 | this.writer.BaseStream.Position = this.lengthPos; 55 | this.writer.Write((uint)sectionLength); 56 | 57 | this.writer.BaseStream.Position = end; 58 | 59 | this.disposed = true; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/IO/EndianBinaryReader.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System; 13 | using System.Buffers.Binary; 14 | using System.IO; 15 | using System.Runtime.CompilerServices; 16 | 17 | namespace PaintShopProFiletype.IO 18 | { 19 | // Adapted from 'Problem and Solution: The Terrible Inefficiency of FileStream and BinaryReader' 20 | // https://jacksondunstan.com/articles/3568 21 | 22 | internal sealed class EndianBinaryReader : PaintDotNet.Disposable 23 | { 24 | private const int MaxBufferSize = 4096; 25 | 26 | private static ReadOnlySpan AsciiWhiteSpaceChars => new byte[] 27 | { 28 | 0x09, // horizontal tab 29 | 0x10, // line feed 30 | 0x11, // vertical tab 31 | 0x12, // form feed 32 | 0x13, // carriage return 33 | 0x20, // space 34 | }; 35 | 36 | #pragma warning disable IDE0032 // Use auto property 37 | private int readOffset; 38 | private int readLength; 39 | 40 | private readonly Stream stream; 41 | private readonly byte[] buffer; 42 | private readonly int bufferSize; 43 | private readonly Endianess endianess; 44 | private readonly bool leaveOpen; 45 | #pragma warning restore IDE0032 // Use auto property 46 | 47 | /// 48 | /// Initializes a new instance of the class. 49 | /// 50 | /// The stream. 51 | /// The byte order of the stream. 52 | /// is null. 53 | public EndianBinaryReader(Stream stream, Endianess byteOrder) : this(stream, byteOrder, false) 54 | { 55 | } 56 | 57 | /// 58 | /// Initializes a new instance of the class. 59 | /// 60 | /// The stream. 61 | /// The byte order of the stream. 62 | /// 63 | /// to leave the stream open after the EndianBinaryReader is disposed; otherwise, 64 | /// 65 | /// is null. 66 | public EndianBinaryReader(Stream stream, Endianess byteOrder, bool leaveOpen) 67 | { 68 | this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); 69 | this.bufferSize = (int)Math.Min(stream.Length, MaxBufferSize); 70 | this.buffer = new byte[this.bufferSize]; 71 | this.endianess = byteOrder; 72 | this.leaveOpen = leaveOpen; 73 | 74 | this.readOffset = 0; 75 | this.readLength = 0; 76 | } 77 | 78 | public Endianess Endianess => this.endianess; 79 | 80 | /// 81 | /// Gets the length of the stream. 82 | /// 83 | /// 84 | /// The length of the stream. 85 | /// 86 | /// The object has been disposed. 87 | public long Length 88 | { 89 | get 90 | { 91 | VerifyNotDisposed(); 92 | 93 | return this.stream.Length; 94 | } 95 | } 96 | 97 | /// 98 | /// Gets or sets the position in the stream. 99 | /// 100 | /// 101 | /// The position in the stream. 102 | /// 103 | /// value is negative. 104 | /// The object has been disposed. 105 | public long Position 106 | { 107 | get 108 | { 109 | VerifyNotDisposed(); 110 | 111 | return this.stream.Position - this.readLength + this.readOffset; 112 | } 113 | set 114 | { 115 | if (value < 0) 116 | { 117 | throw new ArgumentOutOfRangeException(nameof(value)); 118 | } 119 | VerifyNotDisposed(); 120 | 121 | long current = this.Position; 122 | 123 | if (value != current) 124 | { 125 | long bufferStartOffset = current - this.readOffset; 126 | long bufferEndOffset = bufferStartOffset + this.readLength; 127 | 128 | // Avoid reading from the stream if the offset is within the current buffer. 129 | if (value >= bufferStartOffset && value <= bufferEndOffset) 130 | { 131 | this.readOffset = (int)(value - bufferStartOffset); 132 | } 133 | else 134 | { 135 | // Invalidate the existing buffer. 136 | this.readOffset = 0; 137 | this.readLength = 0; 138 | this.stream.Seek(value, SeekOrigin.Begin); 139 | } 140 | } 141 | } 142 | } 143 | 144 | /// 145 | /// Reads the specified number of bytes from the stream, starting from a specified point in the byte array. 146 | /// 147 | /// The bytes. 148 | /// The starting offset in the array. 149 | /// The count. 150 | /// The number of bytes read from the stream. 151 | /// is null. 152 | /// is negative. 153 | /// The end of the stream has been reached. 154 | /// The object has been disposed. 155 | public int Read(byte[] bytes, int offset, int count) 156 | { 157 | if (bytes == null) 158 | { 159 | throw new ArgumentNullException(nameof(bytes)); 160 | } 161 | if (count < 0) 162 | { 163 | throw new ArgumentOutOfRangeException(nameof(count)); 164 | } 165 | VerifyNotDisposed(); 166 | 167 | if (count == 0) 168 | { 169 | return 0; 170 | } 171 | 172 | if ((this.readOffset + count) <= this.readLength) 173 | { 174 | Buffer.BlockCopy(this.buffer, this.readOffset, bytes, offset, count); 175 | this.readOffset += count; 176 | 177 | return count; 178 | } 179 | else 180 | { 181 | // Ensure that any bytes at the end of the current buffer are included. 182 | int bytesUnread = this.readLength - this.readOffset; 183 | 184 | if (bytesUnread > 0) 185 | { 186 | Buffer.BlockCopy(this.buffer, this.readOffset, bytes, offset, bytesUnread); 187 | } 188 | 189 | // Invalidate the existing buffer. 190 | this.readOffset = 0; 191 | this.readLength = 0; 192 | 193 | int totalBytesRead = bytesUnread; 194 | 195 | totalBytesRead += this.stream.Read(bytes, offset + bytesUnread, count - bytesUnread); 196 | 197 | return totalBytesRead; 198 | } 199 | } 200 | 201 | /// 202 | /// Reads the next byte from the current stream. 203 | /// 204 | /// The next byte read from the current stream. 205 | /// The end of the stream has been reached. 206 | /// The object has been disposed. 207 | public byte ReadByte() 208 | { 209 | VerifyNotDisposed(); 210 | 211 | EnsureBuffer(sizeof(byte)); 212 | 213 | byte val = this.buffer[this.readOffset]; 214 | this.readOffset += sizeof(byte); 215 | 216 | return val; 217 | } 218 | 219 | /// 220 | /// Reads the specified number of bytes from the stream. 221 | /// 222 | /// The number of bytes to read.. 223 | /// An array containing the specified bytes. 224 | /// is negative. 225 | /// The end of the stream has been reached. 226 | /// The object has been disposed. 227 | public byte[] ReadBytes(int count) 228 | { 229 | if (count < 0) 230 | { 231 | throw new ArgumentOutOfRangeException(nameof(count)); 232 | } 233 | VerifyNotDisposed(); 234 | 235 | if (count == 0) 236 | { 237 | return Array.Empty(); 238 | } 239 | 240 | byte[] bytes = new byte[count]; 241 | 242 | if ((this.readOffset + count) <= this.readLength) 243 | { 244 | Buffer.BlockCopy(this.buffer, this.readOffset, bytes, 0, count); 245 | this.readOffset += count; 246 | } 247 | else 248 | { 249 | // Ensure that any bytes at the end of the current buffer are included. 250 | int bytesUnread = this.readLength - this.readOffset; 251 | 252 | if (bytesUnread > 0) 253 | { 254 | Buffer.BlockCopy(this.buffer, this.readOffset, bytes, 0, bytesUnread); 255 | } 256 | 257 | int numBytesToRead = count - bytesUnread; 258 | int numBytesRead = bytesUnread; 259 | do 260 | { 261 | int n = this.stream.Read(bytes, numBytesRead, numBytesToRead); 262 | 263 | if (n == 0) 264 | { 265 | throw new EndOfStreamException(); 266 | } 267 | 268 | numBytesRead += n; 269 | numBytesToRead -= n; 270 | 271 | } while (numBytesToRead > 0); 272 | 273 | // Invalidate the existing buffer. 274 | this.readOffset = 0; 275 | this.readLength = 0; 276 | } 277 | 278 | return bytes; 279 | } 280 | 281 | /// 282 | /// Reads a 8-byte floating point value. 283 | /// 284 | /// The 8-byte floating point value. 285 | /// The end of the stream has been reached. 286 | /// The object has been disposed. 287 | public unsafe double ReadDouble() 288 | { 289 | ulong temp = ReadUInt64(); 290 | 291 | return *(double*)&temp; 292 | } 293 | 294 | /// 295 | /// Reads the specified number of bytes from the stream. 296 | /// 297 | /// The destination buffer. 298 | /// The end of the stream has been reached. 299 | /// The object has been disposed. 300 | public void ReadExactly(Span buffer) 301 | { 302 | VerifyNotDisposed(); 303 | 304 | Span destination = buffer; 305 | 306 | while (destination.Length > 0) 307 | { 308 | int bytesRead = ReadInternal(destination); 309 | 310 | if (bytesRead == 0) 311 | { 312 | throw new EndOfStreamException(); 313 | } 314 | 315 | destination = destination.Slice(bytesRead); 316 | } 317 | } 318 | 319 | /// 320 | /// Reads a 2-byte signed integer. 321 | /// 322 | /// The 2-byte signed integer. 323 | /// The end of the stream has been reached. 324 | /// The object has been disposed. 325 | public short ReadInt16() 326 | { 327 | return (short)ReadUInt16(); 328 | } 329 | 330 | /// 331 | /// Reads a 2-byte unsigned integer. 332 | /// 333 | /// The 2-byte unsigned integer. 334 | /// The end of the stream has been reached. 335 | /// The object has been disposed. 336 | public ushort ReadUInt16() 337 | { 338 | VerifyNotDisposed(); 339 | 340 | EnsureBuffer(sizeof(ushort)); 341 | 342 | ushort value = Unsafe.ReadUnaligned(ref this.buffer[this.readOffset]); 343 | 344 | switch (this.endianess) 345 | { 346 | case Endianess.Big: 347 | if (BitConverter.IsLittleEndian) 348 | { 349 | value = BinaryPrimitives.ReverseEndianness(value); 350 | } 351 | break; 352 | case Endianess.Little: 353 | if (!BitConverter.IsLittleEndian) 354 | { 355 | value = BinaryPrimitives.ReverseEndianness(value); 356 | } 357 | break; 358 | default: 359 | throw new InvalidOperationException("Unsupported byte order: " + this.endianess.ToString()); 360 | } 361 | 362 | this.readOffset += sizeof(ushort); 363 | 364 | return value; 365 | } 366 | 367 | /// 368 | /// Reads a 4-byte signed integer. 369 | /// 370 | /// The 4-byte signed integer. 371 | /// The end of the stream has been reached. 372 | /// The object has been disposed. 373 | public int ReadInt32() 374 | { 375 | return (int)ReadUInt32(); 376 | } 377 | 378 | /// 379 | /// Reads a 4-byte unsigned integer. 380 | /// 381 | /// The 4-byte unsigned integer. 382 | /// The end of the stream has been reached. 383 | /// The object has been disposed. 384 | public uint ReadUInt32() 385 | { 386 | VerifyNotDisposed(); 387 | 388 | EnsureBuffer(sizeof(uint)); 389 | 390 | uint value = Unsafe.ReadUnaligned(ref this.buffer[this.readOffset]); 391 | 392 | switch (this.endianess) 393 | { 394 | case Endianess.Big: 395 | if (BitConverter.IsLittleEndian) 396 | { 397 | value = BinaryPrimitives.ReverseEndianness(value); 398 | } 399 | break; 400 | case Endianess.Little: 401 | if (!BitConverter.IsLittleEndian) 402 | { 403 | value = BinaryPrimitives.ReverseEndianness(value); 404 | } 405 | break; 406 | default: 407 | throw new InvalidOperationException("Unsupported byte order: " + this.endianess.ToString()); 408 | } 409 | 410 | this.readOffset += sizeof(uint); 411 | 412 | return value; 413 | } 414 | 415 | /// 416 | /// Reads a 4-byte floating point value. 417 | /// 418 | /// The 4-byte floating point value. 419 | /// The end of the stream has been reached. 420 | /// The object has been disposed. 421 | public unsafe float ReadSingle() 422 | { 423 | uint temp = ReadUInt32(); 424 | 425 | return *(float*)&temp; 426 | } 427 | 428 | /// 429 | /// Reads a 8-byte signed integer. 430 | /// 431 | /// The 8-byte signed integer. 432 | /// The end of the stream has been reached. 433 | /// The object has been disposed. 434 | public long ReadInt64() 435 | { 436 | return (long)ReadUInt64(); 437 | } 438 | 439 | /// 440 | /// Reads a 8-byte unsigned integer. 441 | /// 442 | /// The 8-byte unsigned integer. 443 | /// The end of the stream has been reached. 444 | /// The object has been disposed. 445 | public ulong ReadUInt64() 446 | { 447 | VerifyNotDisposed(); 448 | 449 | EnsureBuffer(sizeof(ulong)); 450 | 451 | ulong value = Unsafe.ReadUnaligned(ref this.buffer[this.readOffset]); 452 | 453 | switch (this.endianess) 454 | { 455 | case Endianess.Big: 456 | if (BitConverter.IsLittleEndian) 457 | { 458 | value = BinaryPrimitives.ReverseEndianness(value); 459 | } 460 | break; 461 | case Endianess.Little: 462 | if (!BitConverter.IsLittleEndian) 463 | { 464 | value = BinaryPrimitives.ReverseEndianness(value); 465 | } 466 | break; 467 | default: 468 | throw new InvalidOperationException("Unsupported byte order: " + this.endianess.ToString()); 469 | } 470 | 471 | this.readOffset += sizeof(ulong); 472 | 473 | return value; 474 | } 475 | 476 | /// 477 | /// Reads an ASCII string from the stream. 478 | /// 479 | /// The length of the string. 480 | /// The string read options. 481 | /// The string. 482 | /// is negative. 483 | /// The end of the stream has been reached. 484 | /// The object has been disposed. 485 | public string ReadAsciiString(int length, StringReadOptions options) 486 | { 487 | if (length < 0) 488 | { 489 | throw new ArgumentOutOfRangeException(nameof(length)); 490 | } 491 | VerifyNotDisposed(); 492 | 493 | if (length == 0) 494 | { 495 | return string.Empty; 496 | } 497 | 498 | ReadOnlySpan bytes; 499 | 500 | if (length <= this.bufferSize) 501 | { 502 | EnsureBuffer(length); 503 | bytes = new ReadOnlySpan(this.buffer, this.readOffset, length); 504 | 505 | this.readOffset += length; 506 | } 507 | else 508 | { 509 | bytes = ReadBytes(length); 510 | } 511 | 512 | if (options.HasFlag(StringReadOptions.TrimNullTerminator)) 513 | { 514 | // Trim the string to the first null-terminator. 515 | // IndexOf should be faster than calling TrimEnd, as the strings this is used with 516 | // will normally be short with a lot of trailing 0 bytes as padding. 517 | int terminatorIndex = bytes.IndexOf((byte)0); 518 | 519 | if (terminatorIndex >= 0) 520 | { 521 | bytes = bytes.Slice(0, terminatorIndex); 522 | } 523 | } 524 | 525 | if (options.HasFlag(StringReadOptions.TrimWhiteSpace)) 526 | { 527 | bytes = bytes.Trim(AsciiWhiteSpaceChars); 528 | } 529 | 530 | return System.Text.Encoding.ASCII.GetString(bytes); 531 | } 532 | 533 | protected override void Dispose(bool disposing) 534 | { 535 | if (disposing && !this.leaveOpen) 536 | { 537 | this.stream.Dispose(); 538 | } 539 | 540 | base.Dispose(disposing); 541 | } 542 | 543 | /// 544 | /// Ensures that the buffer contains at least the number of bytes requested. 545 | /// 546 | /// The minimum number of bytes the buffer should contain. 547 | /// The end of the stream has been reached. 548 | private void EnsureBuffer(int count) 549 | { 550 | if ((this.readOffset + count) > this.readLength) 551 | { 552 | FillBuffer(count); 553 | } 554 | } 555 | 556 | /// 557 | /// Fills the buffer with at least the number of bytes requested. 558 | /// 559 | /// The minimum number of bytes to place in the buffer. 560 | /// The end of the stream has been reached. 561 | private void FillBuffer(int minBytes) 562 | { 563 | if (!TryFillBuffer(minBytes)) 564 | { 565 | ThrowEndOfStreamException(); 566 | } 567 | 568 | static void ThrowEndOfStreamException() 569 | { 570 | throw new EndOfStreamException(); 571 | } 572 | } 573 | 574 | /// 575 | /// Reads the specified number of bytes from the stream. 576 | /// 577 | /// The span. 578 | /// The number of bytes read from the stream. 579 | private int ReadInternal(Span destination) 580 | { 581 | int count = destination.Length; 582 | 583 | if (count == 0) 584 | { 585 | return 0; 586 | } 587 | 588 | if ((this.readOffset + count) <= this.readLength) 589 | { 590 | new ReadOnlySpan(this.buffer, this.readOffset, count).CopyTo(destination); 591 | this.readOffset += count; 592 | 593 | return count; 594 | } 595 | else 596 | { 597 | int totalBytesRead; 598 | 599 | if (count < this.bufferSize) 600 | { 601 | // This is an optimization for sequentially reading small ranges of bytes 602 | // from a file. 603 | // For example, a file signature that will be followed by other header data. 604 | // 605 | // TryFillBuffer may return fewer bytes than were requested if the end of the 606 | // stream has been reached. 607 | totalBytesRead = TryFillBuffer(count) ? count : this.readLength; 608 | 609 | if (totalBytesRead > 0) 610 | { 611 | new ReadOnlySpan(this.buffer, this.readOffset, totalBytesRead).CopyTo(destination.Slice(0, totalBytesRead)); 612 | 613 | this.readOffset += totalBytesRead; 614 | } 615 | } 616 | else 617 | { 618 | // Ensure that any bytes at the end of the current buffer are included. 619 | int bytesUnread = this.readLength - this.readOffset; 620 | 621 | if (bytesUnread > 0) 622 | { 623 | new ReadOnlySpan(this.buffer, this.readOffset, bytesUnread).CopyTo(destination); 624 | } 625 | 626 | totalBytesRead = bytesUnread; 627 | int bytesRemaining = count - bytesUnread; 628 | 629 | // Invalidate the existing buffer. 630 | this.readOffset = 0; 631 | this.readLength = 0; 632 | 633 | totalBytesRead += this.stream.Read(destination.Slice(bytesUnread, bytesRemaining)); 634 | } 635 | 636 | return totalBytesRead; 637 | } 638 | } 639 | 640 | /// 641 | /// Attempts to fill the buffer with at least the number of bytes requested. 642 | /// 643 | /// The minimum number of bytes to place in the buffer. 644 | /// 645 | /// if the buffer contains at least ; otherwise, . 646 | /// 647 | private bool TryFillBuffer(int minBytes) 648 | { 649 | int bytesUnread = this.readLength - this.readOffset; 650 | 651 | if (bytesUnread > 0) 652 | { 653 | Buffer.BlockCopy(this.buffer, this.readOffset, this.buffer, 0, bytesUnread); 654 | } 655 | 656 | this.readOffset = 0; 657 | this.readLength = bytesUnread; 658 | do 659 | { 660 | int bytesRead = this.stream.Read(this.buffer, this.readLength, this.bufferSize - this.readLength); 661 | 662 | if (bytesRead == 0) 663 | { 664 | return false; 665 | } 666 | 667 | this.readLength += bytesRead; 668 | 669 | } while (this.readLength < minBytes); 670 | 671 | return true; 672 | } 673 | 674 | /// 675 | /// Verifies that the has not been disposed. 676 | /// 677 | /// The object has been disposed. 678 | private void VerifyNotDisposed() 679 | { 680 | if (this.IsDisposed) 681 | { 682 | throw new ObjectDisposedException(nameof(EndianBinaryReader)); 683 | } 684 | } 685 | } 686 | } 687 | -------------------------------------------------------------------------------- /src/IO/Endianess.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | namespace PaintShopProFiletype.IO 13 | { 14 | internal enum Endianess 15 | { 16 | Big = 0, 17 | Little 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/IO/StringReadOptions.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | namespace PaintShopProFiletype.IO 13 | { 14 | internal enum StringReadOptions 15 | { 16 | None = 0, 17 | TrimWhiteSpace = (1 << 0), 18 | TrimNullTerminator = (1 << 1), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/IgnoredWords.dic: -------------------------------------------------------------------------------- 1 | attr 2 | config 3 | deserialize 4 | dest 5 | endianess 6 | exif 7 | filetype 8 | gps 9 | idx 10 | ifd 11 | interop 12 | jpg 13 | metadata 14 | nq 15 | pdn 16 | plugin 17 | pspformat 18 | ptr 19 | rect 20 | rgb 21 | RGBQUAD 22 | src 23 | -------------------------------------------------------------------------------- /src/PSPFileType.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System; 13 | using System.Collections.Generic; 14 | using System.IO; 15 | using PaintDotNet; 16 | using PaintDotNet.IndirectUI; 17 | using PaintDotNet.PropertySystem; 18 | using PaintShopProFiletype.Properties; 19 | 20 | namespace PaintShopProFiletype 21 | { 22 | [PluginSupportInfo(typeof(PluginSupportInfo))] 23 | public sealed class PaintShopProFormat : PropertyBasedFileType 24 | { 25 | private static readonly string[] FileExtensions = new string[] { ".psp", ".pspimage", ".pspbrush", ".jfr", ".pspframe", ".pspmask", ".tub", ".psptube" }; 26 | 27 | private readonly IServiceProvider services; 28 | 29 | public PaintShopProFormat(IServiceProvider services) : base( 30 | "Paint Shop Pro", 31 | new FileTypeOptions() 32 | { 33 | SupportsLayers = true, 34 | LoadExtensions = FileExtensions, 35 | SaveExtensions = FileExtensions, 36 | }) 37 | { 38 | this.services = services; 39 | } 40 | 41 | protected override Document OnLoad(Stream input) 42 | { 43 | PSPFile file = new PSPFile(this.services); 44 | return file.Load(input); 45 | } 46 | 47 | public override PropertyCollection OnCreateSavePropertyCollection() 48 | { 49 | List properties = new List() 50 | { 51 | StaticListChoiceProperty.CreateForEnum(PropertyNames.FileVersion, FileVersion.Version6, false), 52 | StaticListChoiceProperty.CreateForEnum(PropertyNames.CompressionType, CompressionFormats.LZ77, false) 53 | }; 54 | 55 | return new PropertyCollection(properties); 56 | } 57 | 58 | public override ControlInfo OnCreateSaveConfigUI(PropertyCollection props) 59 | { 60 | ControlInfo info = PropertyBasedFileType.CreateDefaultSaveConfigUI(props); 61 | 62 | PropertyControlInfo fileVersionPCI = info.FindControlForPropertyName(PropertyNames.FileVersion)!; 63 | 64 | fileVersionPCI.ControlProperties[ControlInfoPropertyNames.DisplayName]!.Value = Resources.FileVersionText; 65 | fileVersionPCI.SetValueDisplayName(FileVersion.Version5, Resources.Version5); 66 | fileVersionPCI.SetValueDisplayName(FileVersion.Version6, Resources.Version6AndLater); 67 | 68 | PropertyControlInfo compressonTypePCI = info.FindControlForPropertyName(PropertyNames.CompressionType)!; 69 | 70 | compressonTypePCI.ControlProperties[ControlInfoPropertyNames.DisplayName]!.Value = Resources.CompressionFormatText; 71 | compressonTypePCI.ControlType.Value = PropertyControlType.RadioButton; 72 | compressonTypePCI.SetValueDisplayName(CompressionFormats.None, Resources.CompressionFormatNoneText); 73 | compressonTypePCI.SetValueDisplayName(CompressionFormats.LZ77, Resources.CompressionFormatLZ77Text); 74 | 75 | return info; 76 | } 77 | 78 | private enum PropertyNames 79 | { 80 | FileVersion, 81 | CompressionType 82 | } 83 | 84 | protected override void OnSaveT(Document input, Stream output, PropertyBasedSaveConfigToken token, Surface scratchSurface, ProgressEventHandler progressCallback) 85 | { 86 | PSPFile file = new PSPFile(this.services); 87 | 88 | CompressionFormats format = (CompressionFormats)token.GetProperty(PropertyNames.CompressionType)!.Value!; 89 | FileVersion version = (FileVersion)token.GetProperty(PropertyNames.FileVersion)!.Value!; 90 | 91 | file.Save(input, output, format, scratchSurface, (ushort)version, progressCallback); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/PSPFileTypeFactory.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using PaintDotNet; 13 | 14 | namespace PaintShopProFiletype 15 | { 16 | public sealed class PSPFileTypeFactory : IFileTypeFactory2 17 | { 18 | public FileType[] GetFileTypeInstances(IFileTypeHost host) 19 | { 20 | return new FileType[] { new PaintShopProFormat(host.Services) }; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PSPSections/ChannelSubBlock.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using CommunityToolkit.HighPerformance; 13 | using CommunityToolkit.HighPerformance.Buffers; 14 | using System; 15 | using System.IO; 16 | using System.IO.Compression; 17 | 18 | namespace PaintShopProFiletype.PSPSections 19 | { 20 | internal sealed class ChannelSubBlock 21 | { 22 | public uint chunkSize; 23 | public uint compressedChannelLength; 24 | public uint uncompressedChannelLength; 25 | public PSPDIBType bitmapType; 26 | public PSPChannelType channelType; 27 | public byte[] channelData; 28 | 29 | private const uint Version6HeaderSize = 16U; 30 | private const uint Version5HeaderSize = 12U; 31 | 32 | public ChannelSubBlock(IO.EndianBinaryReader br, PSPCompression compression, ushort majorVersion) 33 | { 34 | this.chunkSize = majorVersion > PSPConstants.majorVersion5 ? br.ReadUInt32() : 0U; 35 | this.compressedChannelLength = br.ReadUInt32(); 36 | this.uncompressedChannelLength = br.ReadUInt32(); 37 | this.bitmapType = (PSPDIBType)br.ReadUInt16(); 38 | this.channelType = (PSPChannelType)br.ReadUInt16(); 39 | this.channelData = Array.Empty(); 40 | 41 | if (majorVersion > PSPConstants.majorVersion5) 42 | { 43 | long bytesToSkip = (long)this.chunkSize - Version6HeaderSize; 44 | 45 | if (bytesToSkip > 0) 46 | { 47 | br.Position += bytesToSkip; 48 | } 49 | } 50 | 51 | if (this.compressedChannelLength > 0U) 52 | { 53 | switch (compression) 54 | { 55 | case PSPCompression.None: 56 | this.channelData = br.ReadBytes((int)this.compressedChannelLength); 57 | break; 58 | case PSPCompression.RLE: 59 | using (MemoryOwner compressedDataOwner = MemoryOwner.Allocate((int)this.compressedChannelLength)) 60 | { 61 | Span compressedData = compressedDataOwner.Span; 62 | 63 | br.ReadExactly(compressedData); 64 | this.channelData = new byte[this.uncompressedChannelLength]; 65 | 66 | RLE.Decompress(compressedData, this.channelData); 67 | } 68 | break; 69 | case PSPCompression.LZ77: 70 | using (MemoryOwner compressedDataOwner = MemoryOwner.Allocate((int)this.compressedChannelLength)) 71 | { 72 | br.ReadExactly(compressedDataOwner.Span); 73 | this.channelData = new byte[this.uncompressedChannelLength]; 74 | 75 | using (Stream compressedStream = compressedDataOwner.AsStream()) 76 | using (ZLibStream decompressionStream = new ZLibStream(compressedStream, CompressionMode.Decompress)) 77 | { 78 | Span channelDataSpan = this.channelData; 79 | int bytesRead = 0; 80 | 81 | while ((bytesRead = decompressionStream.Read(channelDataSpan)) > 0) 82 | { 83 | channelDataSpan = channelDataSpan.Slice(bytesRead); 84 | } 85 | } 86 | } 87 | break; 88 | default: throw new FormatException($"Unsupported channel compression type: {compression}."); 89 | } 90 | } 91 | } 92 | 93 | public ChannelSubBlock(ushort majorVersion, uint uncompressedSize) 94 | { 95 | this.chunkSize = majorVersion > PSPConstants.majorVersion5 ? Version6HeaderSize : Version5HeaderSize; 96 | this.compressedChannelLength = 0; 97 | this.uncompressedChannelLength = uncompressedSize; 98 | this.bitmapType = PSPDIBType.Image; 99 | this.channelType = PSPChannelType.Composite; 100 | this.channelData = Array.Empty(); 101 | } 102 | 103 | public void Save(BinaryWriter bw, ushort majorVersion) 104 | { 105 | bw.Write(PSPConstants.blockIdentifier); 106 | bw.Write((ushort)PSPBlockID.Channel); 107 | if (majorVersion > PSPConstants.majorVersion5) 108 | { 109 | bw.Write(Version6HeaderSize + this.compressedChannelLength); 110 | bw.Write(this.chunkSize); 111 | } 112 | else 113 | { 114 | bw.Write(Version5HeaderSize); // initial size 115 | bw.Write(Version5HeaderSize + this.compressedChannelLength); 116 | } 117 | bw.Write(this.compressedChannelLength); 118 | bw.Write(this.uncompressedChannelLength); 119 | bw.Write((ushort)this.bitmapType); 120 | bw.Write((ushort)this.channelType); 121 | 122 | if (this.channelData != null) 123 | { 124 | bw.Write(this.channelData); 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/PSPSections/ColorPaletteBlock.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | namespace PaintShopProFiletype.PSPSections 13 | { 14 | internal sealed class ColorPaletteBlock 15 | { 16 | public uint chunkSize; 17 | public uint entriesCount; 18 | 19 | public RGBQUAD[] entries; 20 | 21 | private const uint HeaderSize = 8U; 22 | 23 | public ColorPaletteBlock(IO.EndianBinaryReader br, ushort majorVersion) 24 | { 25 | this.chunkSize = majorVersion > PSPConstants.majorVersion5 ? br.ReadUInt32() : 0; 26 | this.entriesCount = br.ReadUInt32(); 27 | 28 | this.entries = new RGBQUAD[this.entriesCount]; 29 | for (int i = 0; i < this.entriesCount; i++) 30 | { 31 | this.entries[i] = new RGBQUAD(br); 32 | } 33 | 34 | if (majorVersion > PSPConstants.majorVersion5) 35 | { 36 | long bytesToSkip = (long)this.chunkSize - HeaderSize; 37 | 38 | if (bytesToSkip > 0) 39 | { 40 | br.Position += bytesToSkip; 41 | } 42 | } 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/PSPSections/CompositeImageBlock.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using PaintShopProFiletype.IO; 13 | using System; 14 | using System.Diagnostics; 15 | using System.IO; 16 | 17 | namespace PaintShopProFiletype.PSPSections 18 | { 19 | internal sealed class CompositeImageAttributesChunk 20 | { 21 | public uint chunkSize; 22 | public int width; 23 | public int height; 24 | public ushort bitDepth; 25 | public PSPCompression compressionType; // ushort 26 | public ushort planeCount; 27 | public uint colorCount; 28 | public PSPCompositeImageType compositeImageType; // ushort 29 | 30 | private const uint HeaderSize = 24U; 31 | 32 | #if DEBUG 33 | public CompositeImageAttributesChunk(EndianBinaryReader br) 34 | { 35 | this.chunkSize = br.ReadUInt32(); 36 | this.width = br.ReadInt32(); 37 | this.height = br.ReadInt32(); 38 | this.bitDepth = br.ReadUInt16(); 39 | this.compressionType = (PSPCompression)br.ReadUInt16(); 40 | this.planeCount = br.ReadUInt16(); 41 | this.colorCount = br.ReadUInt32(); 42 | this.compositeImageType = (PSPCompositeImageType)br.ReadUInt16(); 43 | 44 | long bytesToSkip = (long)this.chunkSize - HeaderSize; 45 | if (bytesToSkip > 0) 46 | { 47 | br.Position += bytesToSkip; 48 | } 49 | } 50 | #endif 51 | public CompositeImageAttributesChunk(int width, int height, PSPCompositeImageType imageType, PSPCompression compression) 52 | { 53 | this.chunkSize = HeaderSize; 54 | this.width = width; 55 | this.height = height; 56 | this.bitDepth = 24; 57 | this.compressionType = compression; 58 | this.planeCount = 1; 59 | this.colorCount = (1 << 24); 60 | this.compositeImageType = imageType; 61 | } 62 | 63 | public void Save(BinaryWriter bw) 64 | { 65 | bw.Write(PSPConstants.blockIdentifier); 66 | bw.Write((ushort)PSPBlockID.CompositeImageAttributes); 67 | bw.Write(HeaderSize); 68 | bw.Write(this.chunkSize); 69 | bw.Write(this.width); 70 | bw.Write(this.height); 71 | bw.Write(this.bitDepth); 72 | bw.Write((ushort)this.compressionType); 73 | bw.Write(this.planeCount); 74 | bw.Write(this.colorCount); 75 | bw.Write((ushort)this.compositeImageType); 76 | } 77 | } 78 | 79 | internal sealed class JPEGCompositeInfoChunk 80 | { 81 | public uint chunkSize; 82 | public uint compressedSize; 83 | public uint unCompressedSize; 84 | public PSPDIBType imageType; 85 | 86 | public byte[]? imageData; 87 | 88 | private const uint HeaderSize = 14U; 89 | 90 | #if DEBUG 91 | public JPEGCompositeInfoChunk(EndianBinaryReader br) 92 | { 93 | this.chunkSize = br.ReadUInt32(); 94 | this.compressedSize = br.ReadUInt32(); 95 | this.unCompressedSize = br.ReadUInt32(); 96 | this.imageType = (PSPDIBType)br.ReadUInt16(); 97 | 98 | long bytesToSkip = (long)this.chunkSize - HeaderSize; 99 | if (bytesToSkip > 0) 100 | { 101 | br.Position += bytesToSkip; 102 | } 103 | 104 | this.imageData = br.ReadBytes((int)this.compressedSize); 105 | } 106 | #endif 107 | public JPEGCompositeInfoChunk() 108 | { 109 | this.chunkSize = HeaderSize; 110 | this.unCompressedSize = 0; 111 | this.imageType = PSPDIBType.Thumbnail; 112 | this.imageData = null; 113 | } 114 | 115 | public void Save(BinaryWriter bw) 116 | { 117 | bw.Write(PSPConstants.blockIdentifier); 118 | bw.Write((ushort)PSPBlockID.JPEGImage); 119 | bw.Write(HeaderSize + this.compressedSize); 120 | bw.Write(this.chunkSize); 121 | bw.Write(this.compressedSize); 122 | bw.Write(this.unCompressedSize); 123 | bw.Write((ushort)this.imageType); 124 | bw.Write(this.imageData!); 125 | } 126 | } 127 | 128 | internal sealed class CompositeImageInfoChunk 129 | { 130 | public uint chunkSize; 131 | public ushort bitmapCount; 132 | public ushort channelCount; 133 | public ColorPaletteBlock? paletteSubBlock; 134 | public ChannelSubBlock[] channelBlocks; 135 | 136 | private const uint HeaderSize = 8U; 137 | 138 | #if DEBUG 139 | public CompositeImageInfoChunk(EndianBinaryReader br, CompositeImageAttributesChunk attr, ushort majorVersion) 140 | { 141 | this.chunkSize = br.ReadUInt32(); 142 | this.bitmapCount = br.ReadUInt16(); 143 | this.channelCount = br.ReadUInt16(); 144 | this.paletteSubBlock = null; 145 | this.channelBlocks = new ChannelSubBlock[this.channelCount]; 146 | 147 | long bytesToSkip = (long)this.chunkSize - HeaderSize; 148 | if (bytesToSkip > 0) 149 | { 150 | br.Position += bytesToSkip; 151 | } 152 | 153 | int index = 0; 154 | do 155 | { 156 | uint blockSig = br.ReadUInt32(); 157 | if (blockSig != PSPConstants.blockIdentifier) 158 | { 159 | throw new FormatException(Properties.Resources.InvalidBlockSignature); 160 | } 161 | PSPBlockID blockType = (PSPBlockID)br.ReadUInt16(); 162 | #pragma warning disable IDE0059 // Value assigned to symbol is never used 163 | uint chunkLength = br.ReadUInt32(); 164 | #pragma warning restore IDE0059 // Value assigned to symbol is never used 165 | 166 | switch (blockType) 167 | { 168 | case PSPBlockID.ColorPalette: 169 | this.paletteSubBlock = new ColorPaletteBlock(br, majorVersion); 170 | break; 171 | case PSPBlockID.Channel: 172 | ChannelSubBlock block = new ChannelSubBlock(br, attr.compressionType, majorVersion); 173 | this.channelBlocks[index] = block; 174 | index++; 175 | break; 176 | } 177 | } 178 | while (index < this.channelCount); 179 | } 180 | #endif 181 | public CompositeImageInfoChunk() 182 | { 183 | this.chunkSize = HeaderSize; 184 | this.bitmapCount = 1; 185 | this.channelCount = 3; 186 | this.paletteSubBlock = null; 187 | this.channelBlocks = null!; 188 | } 189 | 190 | public void Save(BinaryWriter writer, ushort majorVersion) 191 | { 192 | writer.Write(PSPConstants.blockIdentifier); 193 | writer.Write((ushort)PSPBlockID.CompositeImage); 194 | 195 | using (new BlockLengthWriter(writer)) 196 | { 197 | writer.Write(this.chunkSize); 198 | writer.Write(this.bitmapCount); 199 | writer.Write(this.channelCount); 200 | 201 | for (int i = 0; i < this.channelCount; i++) 202 | { 203 | this.channelBlocks[i].Save(writer, majorVersion); 204 | } 205 | } 206 | } 207 | 208 | } 209 | 210 | internal class CompositeImageBlock 211 | { 212 | private readonly uint blockSize; 213 | private readonly uint attrChunkCount; 214 | private readonly CompositeImageAttributesChunk[] attrChunks; 215 | private readonly JPEGCompositeInfoChunk? jpegChunk; 216 | private readonly CompositeImageInfoChunk? imageChunk; 217 | 218 | private const uint HeaderSize = 8U; 219 | 220 | public CompositeImageBlock(CompositeImageAttributesChunk[] attributes, JPEGCompositeInfoChunk jpg, CompositeImageInfoChunk info) 221 | { 222 | this.blockSize = HeaderSize; 223 | this.attrChunkCount = (uint)attributes.Length; 224 | this.attrChunks = attributes; 225 | this.jpegChunk = jpg; 226 | this.imageChunk = info; 227 | } 228 | 229 | #if DEBUG 230 | public CompositeImageBlock(EndianBinaryReader br, ushort majorVersion) 231 | { 232 | this.blockSize = br.ReadUInt32(); 233 | this.attrChunkCount = br.ReadUInt32(); 234 | 235 | long bytesToSkip = (long)this.blockSize - HeaderSize; 236 | if (bytesToSkip > 0) 237 | { 238 | br.Position += bytesToSkip; 239 | } 240 | 241 | this.attrChunks = new CompositeImageAttributesChunk[(int)this.attrChunkCount]; 242 | 243 | for (int i = 0; i < this.attrChunkCount; i++) 244 | { 245 | uint blockSig = br.ReadUInt32(); 246 | if (blockSig != PSPConstants.blockIdentifier) 247 | { 248 | throw new FormatException(Properties.Resources.InvalidBlockSignature); 249 | } 250 | ushort blockType = br.ReadUInt16(); 251 | PSPUtil.CheckBlockType(blockType, PSPBlockID.CompositeImageAttributes); 252 | #pragma warning disable IDE0059 // Value assigned to symbol is never used 253 | uint attrChunkLength = br.ReadUInt32(); 254 | #pragma warning restore IDE0059 // Value assigned to symbol is never used 255 | 256 | this.attrChunks[i] = new CompositeImageAttributesChunk(br); 257 | } 258 | 259 | for (int i = 0; i < this.attrChunkCount; i++) 260 | { 261 | uint blockSig = br.ReadUInt32(); 262 | if (blockSig != PSPConstants.blockIdentifier) 263 | { 264 | throw new FormatException(Properties.Resources.InvalidBlockSignature); 265 | } 266 | PSPBlockID blockType = (PSPBlockID)br.ReadUInt16(); 267 | #pragma warning disable IDE0059 // Value assigned to symbol is never used 268 | uint chunkLength = br.ReadUInt32(); 269 | #pragma warning restore IDE0059 // Value assigned to symbol is never used 270 | 271 | switch (blockType) 272 | { 273 | case PSPBlockID.CompositeImage: 274 | this.imageChunk = new CompositeImageInfoChunk(br, this.attrChunks[i], majorVersion); 275 | break; 276 | case PSPBlockID.JPEGImage: 277 | this.jpegChunk = new JPEGCompositeInfoChunk(br); 278 | break; 279 | } 280 | } 281 | 282 | if (this.jpegChunk?.imageData != null) 283 | { 284 | using (MemoryStream ms = new MemoryStream(this.jpegChunk.imageData)) 285 | { 286 | using (System.Drawing.Bitmap th = new System.Drawing.Bitmap(ms)) 287 | { 288 | Debug.WriteLine(string.Format("JPEG thumbnail size: {0}x{1}", th.Width, th.Height)); 289 | } 290 | } 291 | } 292 | } 293 | #endif 294 | 295 | /// 296 | /// Saves the CompositeImageBlock to the file. 297 | /// 298 | /// The BinaryWriter to write to. 299 | public void Save(BinaryWriter bw) 300 | { 301 | bw.Write(PSPConstants.blockIdentifier); 302 | bw.Write((ushort)PSPBlockID.CompositeImageBank); 303 | 304 | using (new BlockLengthWriter(bw)) 305 | { 306 | bw.Write(this.blockSize); 307 | bw.Write(this.attrChunkCount); 308 | 309 | foreach (CompositeImageAttributesChunk item in this.attrChunks) 310 | { 311 | item.Save(bw); 312 | } 313 | 314 | this.jpegChunk!.Save(bw); 315 | this.imageChunk!.Save(bw, PSPConstants.majorVersion6); 316 | } 317 | } 318 | 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/PSPSections/CreatorBlock.DebugView.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System; 13 | 14 | #nullable enable 15 | 16 | namespace PaintShopProFiletype.PSPSections 17 | { 18 | internal sealed partial class CreatorBlock 19 | { 20 | private sealed class DebugView 21 | { 22 | private readonly CreatorBlock creatorBlock; 23 | 24 | public DebugView(CreatorBlock creatorBlock) => this.creatorBlock = creatorBlock; 25 | 26 | public string Title => this.creatorBlock.title; 27 | 28 | public DateTime CreateDate => UnixEpochLocal.AddSeconds(this.creatorBlock.createDate); 29 | 30 | public DateTime ModDate => UnixEpochLocal.AddSeconds(this.creatorBlock.modDate); 31 | 32 | public string Artist => this.creatorBlock.artist; 33 | 34 | public string CopyRight => this.creatorBlock.copyRight; 35 | 36 | public string Description => this.creatorBlock.description; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PSPSections/CreatorBlock.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using PaintShopProFiletype.IO; 13 | using System; 14 | using System.Buffers; 15 | using System.Diagnostics; 16 | using System.IO; 17 | using System.Runtime.CompilerServices; 18 | using System.Text; 19 | 20 | namespace PaintShopProFiletype.PSPSections 21 | { 22 | [Serializable] 23 | [DebuggerTypeProxy(typeof(DebugView))] 24 | internal sealed partial class CreatorBlock 25 | { 26 | private static readonly DateTime UnixEpochLocal = new DateTime(1970, 1, 1).ToLocalTime(); 27 | 28 | // These field are required for compatibility with binary serialization. 29 | #pragma warning disable IDE0044 // Add readonly modifier 30 | private string title; 31 | private uint createDate; 32 | [NonSerialized] 33 | private uint modDate; 34 | private string artist; 35 | private string copyRight; 36 | private string description; 37 | #pragma warning restore IDE0044 // Add readonly modifier 38 | 39 | public CreatorBlock(EndianBinaryReader reader, uint blockLength) 40 | { 41 | this.title = string.Empty; 42 | this.createDate = 0; 43 | this.modDate = 0; 44 | this.artist = string.Empty; 45 | this.copyRight = string.Empty; 46 | this.description = string.Empty; 47 | 48 | long endOffset = reader.Position + blockLength; 49 | 50 | while (reader.Position < endOffset && reader.ReadUInt32() == PSPConstants.fieldIdentifier) 51 | { 52 | PSPCreatorFieldID field = (PSPCreatorFieldID)reader.ReadUInt16(); 53 | uint fieldLength = reader.ReadUInt32(); 54 | 55 | switch (field) 56 | { 57 | case PSPCreatorFieldID.Title: 58 | this.title = reader.ReadAsciiString((int)fieldLength, StringReadOptions.TrimWhiteSpace); 59 | break; 60 | case PSPCreatorFieldID.CreateDate: 61 | this.createDate = reader.ReadUInt32(); 62 | break; 63 | case PSPCreatorFieldID.ModifiedDate: 64 | this.modDate = reader.ReadUInt32(); 65 | break; 66 | case PSPCreatorFieldID.Artist: 67 | this.artist = reader.ReadAsciiString((int)fieldLength, StringReadOptions.TrimWhiteSpace); 68 | break; 69 | case PSPCreatorFieldID.Copyright: 70 | this.copyRight = reader.ReadAsciiString((int)fieldLength, StringReadOptions.TrimWhiteSpace); 71 | break; 72 | case PSPCreatorFieldID.Description: 73 | this.description = reader.ReadAsciiString((int)fieldLength, StringReadOptions.TrimWhiteSpace); 74 | break; 75 | #if DEBUG 76 | case PSPCreatorFieldID.ApplicationID: 77 | PSPCreatorAppID appID = (PSPCreatorAppID)reader.ReadUInt32(); 78 | break; 79 | case PSPCreatorFieldID.ApplicationVersion: 80 | uint appVersion = reader.ReadUInt32(); 81 | break; 82 | #endif 83 | default: 84 | reader.Position += fieldLength; 85 | break; 86 | } 87 | } 88 | } 89 | 90 | public string Title => this.title; 91 | 92 | public uint CreateDate => this.createDate; 93 | 94 | public uint ModDate => this.modDate; 95 | 96 | public string Artist => this.artist; 97 | 98 | public string CopyRight => this.copyRight; 99 | 100 | public string Description => this.description; 101 | 102 | public void Save(BinaryWriter writer, ushort majorVersion) 103 | { 104 | writer.Write(PSPConstants.blockIdentifier); 105 | writer.Write((ushort)PSPBlockID.Creator); 106 | 107 | if (majorVersion <= PSPConstants.majorVersion5) 108 | { 109 | writer.Write(0U); // Initial data chunk length, always 0. 110 | } 111 | 112 | using (new BlockLengthWriter(writer)) 113 | { 114 | if (!string.IsNullOrEmpty(this.title)) 115 | { 116 | WriteAsciiField(writer, PSPCreatorFieldID.Title, this.title); 117 | } 118 | 119 | if (this.createDate != 0U) 120 | { 121 | WriteUInt32Field(writer, PSPCreatorFieldID.CreateDate, this.createDate); 122 | } 123 | 124 | WriteUInt32Field(writer, PSPCreatorFieldID.ModifiedDate, GetCurrentUnixTimestamp()); 125 | 126 | if (!string.IsNullOrEmpty(this.artist)) 127 | { 128 | WriteAsciiField(writer, PSPCreatorFieldID.Artist, this.artist); 129 | } 130 | 131 | if (!string.IsNullOrEmpty(this.copyRight)) 132 | { 133 | WriteAsciiField(writer, PSPCreatorFieldID.Copyright, this.copyRight); 134 | } 135 | 136 | if (!string.IsNullOrEmpty(this.description)) 137 | { 138 | WriteAsciiField(writer, PSPCreatorFieldID.Description, this.description); 139 | } 140 | } 141 | } 142 | 143 | /// 144 | /// Gets the current date and time in Unix format (the number of seconds from 1/1/1970) 145 | /// 146 | /// The current date and time in Unix format 147 | private static uint GetCurrentUnixTimestamp() 148 | { 149 | TimeSpan t = (DateTime.Now - UnixEpochLocal); 150 | 151 | return (uint)t.TotalSeconds; 152 | } 153 | 154 | [SkipLocalsInit] 155 | private static void WriteAsciiField(BinaryWriter writer, PSPCreatorFieldID field, string value) 156 | { 157 | const int MaxStackBufferLength = 256; 158 | 159 | Span buffer = stackalloc byte[MaxStackBufferLength]; 160 | 161 | byte[]? arrayFromPool = null; 162 | 163 | try 164 | { 165 | int maxNameLengthInBytes = Encoding.ASCII.GetMaxByteCount(value.Length); 166 | 167 | if (maxNameLengthInBytes > MaxStackBufferLength) 168 | { 169 | arrayFromPool = ArrayPool.Shared.Rent(maxNameLengthInBytes); 170 | buffer = arrayFromPool; 171 | } 172 | 173 | int bytesWritten = Encoding.ASCII.GetBytes(value, buffer); 174 | 175 | buffer = buffer.Slice(0, bytesWritten); 176 | 177 | writer.Write(PSPConstants.fieldIdentifier); 178 | writer.Write((ushort)field); 179 | writer.Write((uint)buffer.Length); 180 | writer.Write(buffer); 181 | } 182 | finally 183 | { 184 | if (arrayFromPool != null) 185 | { 186 | ArrayPool.Shared.Return(arrayFromPool); 187 | } 188 | } 189 | } 190 | 191 | private static void WriteUInt32Field(BinaryWriter writer, PSPCreatorFieldID field, uint value) 192 | { 193 | writer.Write(PSPConstants.fieldIdentifier); 194 | writer.Write((ushort)field); 195 | writer.Write(sizeof(uint)); 196 | writer.Write(value); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/PSPSections/CreatorBlockSerialization.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using PaintDotNet; 13 | using System; 14 | using System.IO; 15 | using System.Runtime.Serialization; 16 | using System.Runtime.Serialization.Formatters.Binary; 17 | using System.Text; 18 | using System.Xml; 19 | 20 | #nullable enable 21 | 22 | namespace PaintShopProFiletype.PSPSections 23 | { 24 | internal static class CreatorBlockSerialization 25 | { 26 | private const string PSPCreatorMetaDataBinaryFormatter = "PSPFormatCreatorData"; 27 | private const string PSPCreatorMetaDataDataContract = "PSPFormatCreatorData2"; 28 | 29 | public static CreatorBlock? Deserialize(Document input) 30 | { 31 | CreatorBlock? result = null; 32 | 33 | string? creatorData = input.Metadata.GetUserValue(PSPCreatorMetaDataDataContract); 34 | if (!string.IsNullOrEmpty(creatorData)) 35 | { 36 | result = DeserializeDataContract(creatorData); 37 | } 38 | else 39 | { 40 | creatorData = input.Metadata.GetUserValue(PSPCreatorMetaDataBinaryFormatter); 41 | if (!string.IsNullOrEmpty(creatorData)) 42 | { 43 | result = DeserializeBinaryFormatter(creatorData); 44 | } 45 | } 46 | 47 | return result; 48 | } 49 | 50 | public static void Serialize(CreatorBlock? input, Document document) 51 | { 52 | if (input is null) 53 | { 54 | return; 55 | } 56 | 57 | string data = SerializeDataContract(input); 58 | 59 | document.Metadata.SetUserValue(PSPCreatorMetaDataDataContract, data); 60 | } 61 | 62 | private static CreatorBlock DeserializeBinaryFormatter(string base64) 63 | { 64 | byte[] bytes = Convert.FromBase64String(base64); 65 | 66 | using (MemoryStream stream = new MemoryStream(bytes)) 67 | { 68 | #pragma warning disable SYSLIB0011 69 | BinaryFormatter formatter = new BinaryFormatter() { Binder = new SelfBinder() }; 70 | return (CreatorBlock)formatter.Deserialize(stream); 71 | #pragma warning restore SYSLIB0011 72 | } 73 | } 74 | 75 | private static CreatorBlock DeserializeDataContract(string text) 76 | { 77 | CreatorBlock result; 78 | 79 | using (StringReader stringReader = new StringReader(text)) 80 | using (XmlReader xmlReader = XmlReader.Create(stringReader)) 81 | { 82 | result = (CreatorBlock)new DataContractSerializer(typeof(CreatorBlock)).ReadObject(xmlReader)!; 83 | } 84 | 85 | return result; 86 | } 87 | 88 | private static string SerializeDataContract(CreatorBlock creatorBlock) 89 | { 90 | StringBuilder builder = new StringBuilder(); 91 | 92 | using (XmlWriter writer = XmlWriter.Create(builder)) 93 | { 94 | new DataContractSerializer(typeof(CreatorBlock)).WriteObject(writer, creatorBlock); 95 | } 96 | 97 | return builder.ToString(); 98 | } 99 | 100 | /// 101 | /// Binds the serialization to types in the currently loaded assembly. 102 | /// 103 | private class SelfBinder : SerializationBinder 104 | { 105 | public override Type? BindToType(string assemblyName, string typeName) 106 | { 107 | return Type.GetType(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0},{1}", typeName, assemblyName)); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/PSPSections/CreatorBlockSerializedData.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System.Runtime.Serialization; 13 | 14 | #nullable enable 15 | 16 | namespace PaintShopProFiletype.PSPSections 17 | { 18 | [DataContract] 19 | internal sealed class CreatorBlockSerializedData 20 | { 21 | [DataMember(Order = 0)] 22 | private string title; 23 | [DataMember(Order = 1)] 24 | private uint createDate; 25 | [DataMember(Order = 2)] 26 | private string artist; 27 | [DataMember(Order = 3)] 28 | private string copyRight; 29 | [DataMember(Order = 4)] 30 | private string description; 31 | 32 | public CreatorBlockSerializedData() 33 | { 34 | this.title = string.Empty; 35 | this.createDate = 0; 36 | this.artist = string.Empty; 37 | this.copyRight = string.Empty; 38 | this.description = string.Empty; 39 | } 40 | 41 | public CreatorBlockSerializedData(CreatorBlock creatorBlock) 42 | { 43 | this.title = creatorBlock.Title; 44 | this.createDate = creatorBlock.CreateDate; 45 | this.artist = creatorBlock.Artist; 46 | this.copyRight = creatorBlock.CopyRight; 47 | this.description = creatorBlock.Description; 48 | } 49 | 50 | public string Title => this.title; 51 | 52 | public uint CreateDate => this.createDate; 53 | 54 | public string Artist => this.artist; 55 | 56 | public string CopyRight => this.copyRight; 57 | 58 | public string Description => this.description; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/PSPSections/ExtendedDataBlock.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using PaintShopProFiletype.IO; 13 | using System; 14 | using System.Collections.Generic; 15 | 16 | namespace PaintShopProFiletype.PSPSections 17 | { 18 | internal class ExtendedDataBlock 19 | { 20 | private readonly uint blockLength; 21 | private readonly List> values; 22 | 23 | public ExtendedDataBlock(EndianBinaryReader br, uint blockLength) 24 | { 25 | this.blockLength = blockLength; 26 | this.values = new List>(); 27 | Load(br); 28 | } 29 | 30 | public short TryGetTransparencyIndex() 31 | { 32 | short index = -1; 33 | 34 | foreach (var item in this.values) 35 | { 36 | if (item.Key == PSPExtendedDataID.TransparencyIndex) 37 | { 38 | index = BitConverter.ToInt16(item.Value, 0); 39 | break; 40 | } 41 | } 42 | 43 | return index; 44 | } 45 | 46 | private void Load(EndianBinaryReader br) 47 | { 48 | long startOffset = br.Position; 49 | 50 | long endOffset = startOffset + this.blockLength; 51 | 52 | while ((br.Position < endOffset) && br.ReadUInt32() == PSPConstants.fieldIdentifier) 53 | { 54 | PSPExtendedDataID fieldID = (PSPExtendedDataID)br.ReadUInt16(); 55 | 56 | uint fieldLength = br.ReadUInt32(); 57 | 58 | byte[] data = br.ReadBytes((int)fieldLength); 59 | 60 | this.values.Add(new KeyValuePair(fieldID, data)); 61 | } 62 | 63 | long bytesToSkip = this.blockLength - (br.Position - startOffset); 64 | 65 | if (bytesToSkip > 0) 66 | { 67 | br.Position += bytesToSkip; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/PSPSections/FileHeader.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System.IO; 13 | 14 | namespace PaintShopProFiletype.PSPSections 15 | { 16 | internal class FileHeader 17 | { 18 | #pragma warning disable IDE0032 // Use auto property 19 | private readonly ushort majorVersion; 20 | private readonly ushort minorVersion; 21 | #pragma warning restore IDE0032 // Use auto property 22 | 23 | public ushort Major 24 | { 25 | get 26 | { 27 | return this.majorVersion; 28 | } 29 | } 30 | 31 | public ushort Minor 32 | { 33 | get 34 | { 35 | return this.minorVersion; 36 | } 37 | } 38 | 39 | public FileHeader(ushort major) 40 | { 41 | this.majorVersion = major; 42 | this.minorVersion = 0; 43 | } 44 | 45 | public FileHeader(IO.EndianBinaryReader br) 46 | { 47 | this.majorVersion = br.ReadUInt16(); 48 | this.minorVersion = br.ReadUInt16(); 49 | } 50 | 51 | public void Save(BinaryWriter bw) 52 | { 53 | bw.Write(this.majorVersion); 54 | bw.Write(this.minorVersion); 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PSPSections/GeneralImageAttributes.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using PaintShopProFiletype.IO; 13 | using System.IO; 14 | 15 | namespace PaintShopProFiletype.PSPSections 16 | { 17 | internal class GeneralImageAttributes 18 | { 19 | #pragma warning disable IDE0032 // Use auto property 20 | private uint chunkSize; 21 | private int width; 22 | private int height; 23 | private double resValue; 24 | private ResolutionMetric resUnits; // byte 25 | private PSPCompression compressionType; // ushort 26 | private ushort bitDepth; 27 | private ushort planeCount; 28 | private uint colorCount; 29 | private byte grayScale; 30 | private uint totalImageSize; 31 | private int activeLayer; 32 | private ushort layerCount; 33 | private PSPGraphicContents graphicContents; 34 | #pragma warning restore IDE0032 // Use auto property 35 | 36 | public int Width 37 | { 38 | get 39 | { 40 | return this.width; 41 | } 42 | } 43 | public int Height 44 | { 45 | get 46 | { 47 | return this.height; 48 | } 49 | } 50 | public double ResValue 51 | { 52 | get 53 | { 54 | return this.resValue; 55 | } 56 | set 57 | { 58 | this.resValue = value; 59 | } 60 | } 61 | 62 | public ResolutionMetric ResUnit 63 | { 64 | get 65 | { 66 | return this.resUnits; 67 | } 68 | set 69 | { 70 | this.resUnits = value; 71 | } 72 | } 73 | 74 | public PSPCompression CompressionType 75 | { 76 | get 77 | { 78 | return this.compressionType; 79 | } 80 | } 81 | 82 | public ushort BitDepth 83 | { 84 | get 85 | { 86 | return this.bitDepth; 87 | } 88 | } 89 | 90 | public int LayerCount 91 | { 92 | get 93 | { 94 | return this.layerCount; 95 | } 96 | } 97 | 98 | private const uint Version6HeaderSize = 46U; 99 | private const uint Version5HeaderSize = 38U; 100 | 101 | private readonly ushort fileMajorVersion; 102 | 103 | public GeneralImageAttributes(int width, int height, PSPCompression compType, int activeLayer, int layerCount, ushort majorVersion) 104 | { 105 | this.chunkSize = majorVersion > PSPConstants.majorVersion5 ? Version6HeaderSize : Version5HeaderSize; 106 | this.width = width; 107 | this.height = height; 108 | this.compressionType = compType; 109 | this.activeLayer = activeLayer; 110 | this.layerCount = (ushort)layerCount; 111 | this.bitDepth = 24; 112 | this.colorCount = (1U << this.bitDepth); 113 | this.graphicContents |= PSPGraphicContents.RasterLayers; 114 | 115 | this.fileMajorVersion = majorVersion; 116 | } 117 | 118 | public GeneralImageAttributes(EndianBinaryReader br, ushort majorVersion) 119 | { 120 | this.fileMajorVersion = majorVersion; 121 | 122 | Load(br); 123 | } 124 | 125 | private void Load(EndianBinaryReader br) 126 | { 127 | this.chunkSize = this.fileMajorVersion > PSPConstants.majorVersion5 ? br.ReadUInt32() : 0; 128 | this.width = br.ReadInt32(); 129 | this.height = br.ReadInt32(); 130 | this.resValue = br.ReadDouble(); 131 | this.resUnits = (ResolutionMetric)br.ReadByte(); 132 | this.compressionType = (PSPCompression)br.ReadUInt16(); 133 | this.bitDepth = br.ReadUInt16(); 134 | this.planeCount = br.ReadUInt16(); 135 | this.colorCount = br.ReadUInt32(); 136 | this.grayScale = br.ReadByte(); 137 | this.totalImageSize = br.ReadUInt32(); 138 | this.activeLayer = br.ReadInt32(); 139 | this.layerCount = br.ReadUInt16(); 140 | 141 | if (this.fileMajorVersion > PSPConstants.majorVersion5) 142 | { 143 | this.graphicContents = (PSPGraphicContents)br.ReadUInt32(); 144 | } 145 | else 146 | { 147 | this.graphicContents = PSPGraphicContents.None; 148 | } 149 | 150 | if (this.fileMajorVersion > PSPConstants.majorVersion5) 151 | { 152 | long bytesToSkip = (long)this.chunkSize - Version6HeaderSize; 153 | 154 | if (bytesToSkip > 0) 155 | { 156 | br.Position += bytesToSkip; 157 | } 158 | } 159 | } 160 | 161 | public void Save(BinaryWriter bw) 162 | { 163 | bw.Write(PSPConstants.blockIdentifier); 164 | bw.Write((ushort)PSPBlockID.ImageAttributes); 165 | if (this.fileMajorVersion > PSPConstants.majorVersion5) 166 | { 167 | bw.Write(Version6HeaderSize); // total size 168 | bw.Write(this.chunkSize); // total size 169 | } 170 | else 171 | { 172 | bw.Write(Version5HeaderSize); // initial size 173 | bw.Write(Version5HeaderSize); // total size 174 | } 175 | bw.Write(this.width); 176 | bw.Write(this.height); 177 | bw.Write(this.resValue); 178 | bw.Write((byte)this.resUnits); 179 | bw.Write((ushort)this.compressionType); 180 | bw.Write(this.bitDepth); 181 | bw.Write(this.planeCount); 182 | bw.Write(this.colorCount); 183 | bw.Write(this.grayScale); 184 | bw.Write(this.totalImageSize); 185 | bw.Write(this.activeLayer); 186 | bw.Write(this.layerCount); 187 | 188 | if (this.fileMajorVersion > PSPConstants.majorVersion5) 189 | { 190 | bw.Write((uint)this.graphicContents); 191 | } 192 | } 193 | 194 | public void SetGraphicContentFlag(PSPGraphicContents flag) 195 | { 196 | this.graphicContents |= flag; 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/PSPSections/LayerBlock.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using PaintShopProFiletype.IO; 13 | using System; 14 | using System.Buffers; 15 | using System.Collections.Generic; 16 | using System.Drawing; 17 | using System.IO; 18 | using System.Runtime.CompilerServices; 19 | using System.Text; 20 | 21 | namespace PaintShopProFiletype.PSPSections 22 | { 23 | internal readonly struct BlendRange 24 | { 25 | public readonly uint sourceRange; 26 | public readonly uint destRange; 27 | 28 | public BlendRange(EndianBinaryReader reader) 29 | { 30 | this.sourceRange = reader.ReadUInt32(); 31 | this.destRange = reader.ReadUInt32(); 32 | } 33 | 34 | public BlendRange(uint sourceRange, uint destRange) 35 | { 36 | this.sourceRange = sourceRange; 37 | this.destRange = destRange; 38 | } 39 | 40 | public void Write(BinaryWriter writer) 41 | { 42 | writer.Write(this.sourceRange); 43 | writer.Write(this.destRange); 44 | } 45 | } 46 | 47 | internal sealed class LayerBitmapInfoChunk 48 | { 49 | public uint chunkSize; 50 | public ushort bitmapCount; 51 | public ushort channelCount; 52 | public ChannelSubBlock[] channels; 53 | 54 | internal const uint HeaderSize = 8U; 55 | 56 | public LayerBitmapInfoChunk(EndianBinaryReader br, PSPCompression compression, ushort majorVersion) 57 | { 58 | long startOffset = br.Position; 59 | 60 | this.chunkSize = br.ReadUInt32(); 61 | this.bitmapCount = br.ReadUInt16(); 62 | this.channelCount = br.ReadUInt16(); 63 | this.channels = new ChannelSubBlock[this.channelCount]; 64 | 65 | long bytesToSkip = this.chunkSize - (br.Position - startOffset); 66 | if (bytesToSkip > 0L) 67 | { 68 | br.Position += bytesToSkip; 69 | } 70 | 71 | for (int i = 0; i < this.channelCount; i++) 72 | { 73 | uint head = br.ReadUInt32(); 74 | if (head != PSPConstants.blockIdentifier) 75 | { 76 | throw new FormatException(Properties.Resources.InvalidBlockSignature); 77 | } 78 | ushort blockID = br.ReadUInt16(); 79 | PSPUtil.CheckBlockType(blockID, PSPBlockID.Channel); 80 | #pragma warning disable IDE0059 // Value assigned to symbol is never used 81 | uint size = br.ReadUInt32(); 82 | #pragma warning restore IDE0059 // Value assigned to symbol is never used 83 | 84 | this.channels[i] = new ChannelSubBlock(br, compression, majorVersion); 85 | } 86 | } 87 | 88 | public LayerBitmapInfoChunk(EndianBinaryReader br, PSPCompression compression, ushort v5BitmapCount, ushort v5ChannelCount) 89 | { 90 | this.chunkSize = 0; 91 | this.bitmapCount = v5BitmapCount; 92 | this.channelCount = v5ChannelCount; 93 | this.channels = new ChannelSubBlock[this.channelCount]; 94 | 95 | for (int i = 0; i < this.channelCount; i++) 96 | { 97 | uint head = br.ReadUInt32(); 98 | if (head != PSPConstants.blockIdentifier) 99 | { 100 | throw new FormatException(Properties.Resources.InvalidBlockSignature); 101 | } 102 | ushort blockID = br.ReadUInt16(); 103 | PSPUtil.CheckBlockType(blockID, PSPBlockID.Channel); 104 | #pragma warning disable IDE0059 // Value assigned to symbol is never used 105 | uint initialSize = br.ReadUInt32(); 106 | uint size = br.ReadUInt32(); 107 | #pragma warning restore IDE0059 // Value assigned to symbol is never used 108 | 109 | this.channels[i] = new ChannelSubBlock(br, compression, PSPConstants.majorVersion5); 110 | } 111 | } 112 | 113 | public LayerBitmapInfoChunk(int bitmapCount, ChannelSubBlock[] channels) 114 | { 115 | this.chunkSize = HeaderSize; 116 | this.bitmapCount = (ushort)bitmapCount; 117 | this.channelCount = checked((ushort)channels.Length); 118 | this.channels = channels; 119 | } 120 | 121 | public void Save(BinaryWriter bw, ushort majorVersion) 122 | { 123 | if (majorVersion > PSPConstants.majorVersion5) 124 | { 125 | bw.Write(this.chunkSize); 126 | bw.Write(this.bitmapCount); 127 | bw.Write(this.channelCount); 128 | } 129 | 130 | for (int i = 0; i < this.channelCount; i++) 131 | { 132 | this.channels![i].Save(bw, majorVersion); 133 | } 134 | } 135 | } 136 | 137 | internal sealed class LayerInfoChunk 138 | { 139 | internal const uint Version5ChunkLength = 375U; 140 | /// 141 | /// The base chunk size of the version 6 header, 119 bytes + 2 for the name length. 142 | /// 143 | private const uint Version6BaseChunkSize = 119 + sizeof(ushort); 144 | private const int BlendRangeCount = 5; 145 | 146 | public uint chunkSize; 147 | public string name; 148 | public PSPLayerType type; 149 | public Rectangle imageRect; // Int32 150 | public Rectangle saveRect; // Int32 151 | public byte opacity; 152 | public PSPBlendModes blendMode; // byte 153 | public PSPLayerProperties layerFlags; 154 | public byte protectedTransparency; 155 | public byte linkGroup; 156 | public Rectangle maskRect; 157 | public Rectangle saveMaskRect; 158 | public byte maskLinked; 159 | public byte maskDisabled; 160 | public byte invertMaskOnBlend; 161 | public ushort blendRangeCount; 162 | public BlendRange[] blendRanges; 163 | 164 | public ushort v5BitmapCount; 165 | public ushort v5ChannelCount; 166 | 167 | public byte useHighlightColor; 168 | public uint highlightColor; 169 | 170 | public LayerInfoChunk(EndianBinaryReader br, ushort majorVersion) 171 | { 172 | long startOffset = br.Position; 173 | 174 | if (majorVersion > PSPConstants.majorVersion5) 175 | { 176 | this.chunkSize = br.ReadUInt32(); 177 | ushort nameLen = br.ReadUInt16(); 178 | this.name = br.ReadAsciiString(nameLen, StringReadOptions.None); 179 | } 180 | else 181 | { 182 | this.chunkSize = 0; 183 | this.name = br.ReadAsciiString(256, StringReadOptions.TrimNullTerminator); 184 | } 185 | 186 | this.type = (PSPLayerType)br.ReadByte(); 187 | 188 | if (majorVersion <= PSPConstants.majorVersion5 && this.type == PSPLayerType.Undefined) 189 | { 190 | this.type = PSPLayerType.Raster; 191 | } 192 | 193 | this.imageRect = Rectangle.FromLTRB(br.ReadInt32(), br.ReadInt32(), br.ReadInt32(), br.ReadInt32()); 194 | this.saveRect = Rectangle.FromLTRB(br.ReadInt32(), br.ReadInt32(), br.ReadInt32(), br.ReadInt32()); 195 | this.opacity = br.ReadByte(); 196 | this.blendMode = (PSPBlendModes)br.ReadByte(); 197 | this.layerFlags = (PSPLayerProperties)br.ReadByte(); 198 | this.protectedTransparency = br.ReadByte(); 199 | this.linkGroup = br.ReadByte(); 200 | this.maskRect = Rectangle.FromLTRB(br.ReadInt32(), br.ReadInt32(), br.ReadInt32(), br.ReadInt32()); 201 | this.saveMaskRect = Rectangle.FromLTRB(br.ReadInt32(), br.ReadInt32(), br.ReadInt32(), br.ReadInt32()); 202 | this.maskLinked = br.ReadByte(); 203 | this.maskDisabled = br.ReadByte(); 204 | this.invertMaskOnBlend = br.ReadByte(); 205 | this.blendRangeCount = br.ReadUInt16(); 206 | this.blendRanges = new BlendRange[BlendRangeCount]; 207 | for (int i = 0; i < this.blendRanges.Length; i++) 208 | { 209 | this.blendRanges[i] = new BlendRange(br); 210 | } 211 | this.v5BitmapCount = 0; 212 | this.v5ChannelCount = 0; 213 | this.useHighlightColor = 0; 214 | this.highlightColor = 0; 215 | if (majorVersion >= PSPConstants.majorVersion8) 216 | { 217 | this.useHighlightColor = br.ReadByte(); 218 | this.highlightColor = br.ReadUInt32(); 219 | } 220 | else if (majorVersion <= PSPConstants.majorVersion5) 221 | { 222 | this.v5BitmapCount = br.ReadUInt16(); 223 | this.v5ChannelCount = br.ReadUInt16(); 224 | } 225 | 226 | if (majorVersion > PSPConstants.majorVersion5) 227 | { 228 | long bytesToSkip = this.chunkSize - (br.Position - startOffset); 229 | if (bytesToSkip > 0) 230 | { 231 | br.Position += bytesToSkip; 232 | } 233 | } 234 | } 235 | 236 | public LayerInfoChunk(PaintDotNet.Layer layer, PSPBlendModes blendMode, Rectangle savedBounds, ushort majorVersion) 237 | { 238 | if (majorVersion > PSPConstants.majorVersion5) 239 | { 240 | this.chunkSize = Version6BaseChunkSize + (uint)Encoding.ASCII.GetByteCount(layer.Name); 241 | } 242 | else 243 | { 244 | this.chunkSize = 0U; 245 | } 246 | this.name = layer.Name; 247 | this.type = majorVersion > PSPConstants.majorVersion5 ? PSPLayerType.Raster : 0; 248 | this.imageRect = layer.Bounds; 249 | this.saveRect = savedBounds; 250 | this.opacity = layer.Opacity; 251 | this.blendMode = blendMode; 252 | this.layerFlags = PSPLayerProperties.None; 253 | if (layer.Visible) 254 | { 255 | this.layerFlags |= PSPLayerProperties.Visible; 256 | } 257 | this.protectedTransparency = 0; 258 | this.linkGroup = 0; 259 | this.maskRect = Rectangle.Empty; 260 | this.saveMaskRect = Rectangle.Empty; 261 | this.maskLinked = 0; 262 | this.invertMaskOnBlend = 0; 263 | this.blendRangeCount = 0; 264 | this.blendRanges = new BlendRange[BlendRangeCount]; 265 | this.v5BitmapCount = 0; 266 | this.v5ChannelCount = 0; 267 | this.useHighlightColor = 0; 268 | this.highlightColor = 0; 269 | } 270 | 271 | public void Save(BinaryWriterEx bw, ushort majorVersion) 272 | { 273 | if (majorVersion > PSPConstants.majorVersion5) 274 | { 275 | bw.Write(this.chunkSize); 276 | } 277 | WriteLayerName(bw, majorVersion); 278 | bw.Write((byte)this.type); 279 | bw.Write(this.imageRect); 280 | bw.Write(this.saveRect); 281 | bw.Write(this.opacity); 282 | bw.Write((byte)this.blendMode); 283 | bw.Write((byte)this.layerFlags); 284 | bw.Write(this.protectedTransparency); 285 | bw.Write(this.linkGroup); 286 | bw.Write(this.maskRect); 287 | bw.Write(this.saveMaskRect); 288 | bw.Write(this.maskLinked); 289 | bw.Write(this.maskDisabled); 290 | bw.Write(this.invertMaskOnBlend); 291 | bw.Write(this.blendRangeCount); 292 | for (int i = 0; i < this.blendRanges.Length; i++) 293 | { 294 | this.blendRanges[i].Write(bw); 295 | } 296 | if (majorVersion <= PSPConstants.majorVersion5) 297 | { 298 | bw.Write(this.v5BitmapCount); 299 | bw.Write(this.v5ChannelCount); 300 | } 301 | } 302 | 303 | [SkipLocalsInit] 304 | private void WriteLayerName(BinaryWriter writer, ushort majorVersion) 305 | { 306 | const int MaxStackBufferLength = 256; 307 | 308 | Span buffer = stackalloc byte[MaxStackBufferLength]; 309 | 310 | byte[]? arrayFromPool = null; 311 | 312 | try 313 | { 314 | int maxNameLengthInBytes = Encoding.ASCII.GetMaxByteCount(this.name.Length); 315 | 316 | if (maxNameLengthInBytes > MaxStackBufferLength) 317 | { 318 | arrayFromPool = ArrayPool.Shared.Rent(maxNameLengthInBytes); 319 | buffer = arrayFromPool; 320 | } 321 | 322 | int bytesWritten = Encoding.ASCII.GetBytes(this.name, buffer); 323 | 324 | if (majorVersion > PSPConstants.majorVersion5) 325 | { 326 | buffer = buffer.Slice(0, bytesWritten); 327 | 328 | writer.Write(checked((ushort)buffer.Length)); 329 | writer.Write(buffer); 330 | } 331 | else 332 | { 333 | // Paint Shop Pro 5 and earlier use a null-terminated layer name string in a fixed 256 byte 334 | // buffer, so strings that are longer than 255 bytes will be truncated to ensure that the 335 | // null-terminator can be written. 336 | buffer = buffer.Slice(0, 256); 337 | 338 | int terminatorIndex = Math.Min(bytesWritten, 255); 339 | 340 | // Fill the remaining space in the buffer with zeros. 341 | buffer.Slice(terminatorIndex).Clear(); 342 | 343 | writer.Write(buffer); 344 | } 345 | } 346 | finally 347 | { 348 | if (arrayFromPool != null) 349 | { 350 | ArrayPool.Shared.Return(arrayFromPool); 351 | } 352 | } 353 | } 354 | } 355 | 356 | internal sealed class LayerBlock 357 | { 358 | #pragma warning disable IDE0032 // Use auto property 359 | private readonly LayerInfoChunk[] layerInfoChunks; 360 | private readonly LayerBitmapInfoChunk[] layerBitmapInfo; 361 | #pragma warning restore IDE0032 // Use auto property 362 | 363 | public LayerInfoChunk[] LayerInfo 364 | { 365 | get 366 | { 367 | return this.layerInfoChunks; 368 | } 369 | } 370 | 371 | public LayerBitmapInfoChunk[] LayerBitmapInfo 372 | { 373 | get 374 | { 375 | return this.layerBitmapInfo; 376 | } 377 | } 378 | 379 | public LayerBlock(LayerInfoChunk[] infoChunks, LayerBitmapInfoChunk[] biChunks) 380 | { 381 | this.layerInfoChunks = infoChunks; 382 | this.layerBitmapInfo = biChunks; 383 | } 384 | 385 | public LayerBlock(EndianBinaryReader br, GeneralImageAttributes? imageAttributes, ushort majorVersion) 386 | { 387 | if (imageAttributes is null) 388 | { 389 | // The image attributes block must come before the layer block. 390 | throw new FormatException(Properties.Resources.InvalidPSPFile); 391 | } 392 | 393 | IList raster = CountRasterChunks(br, imageAttributes.LayerCount, majorVersion); 394 | 395 | int layerCount = raster.Count; 396 | 397 | if (layerCount == 0) 398 | { 399 | throw new FormatException(Properties.Resources.RasterLayerNotFound); 400 | } 401 | 402 | this.layerInfoChunks = new LayerInfoChunk[layerCount]; 403 | this.layerBitmapInfo = new LayerBitmapInfoChunk[layerCount]; 404 | PSPCompression compression = imageAttributes.CompressionType; 405 | 406 | for (int i = 0; i < layerCount; i++) 407 | { 408 | RasterLayerChunk chunk = raster[i]; 409 | this.layerInfoChunks[i] = chunk.layerInfo; 410 | 411 | if (!chunk.layerInfo.saveRect.IsEmpty) 412 | { 413 | br.Position = chunk.bitmapInfoOffset; 414 | if (majorVersion <= PSPConstants.majorVersion5) 415 | { 416 | this.layerBitmapInfo[i] = new LayerBitmapInfoChunk(br, compression, chunk.layerInfo.v5BitmapCount, chunk.layerInfo.v5ChannelCount); 417 | } 418 | else 419 | { 420 | this.layerBitmapInfo[i] = new LayerBitmapInfoChunk(br, compression, majorVersion); 421 | } 422 | } 423 | } 424 | } 425 | 426 | public void Save(BinaryWriterEx bw, ushort majorVersion) 427 | { 428 | bw.Write(PSPConstants.blockIdentifier); 429 | bw.Write((ushort)PSPBlockID.LayerStart); 430 | if (majorVersion <= PSPConstants.majorVersion5) 431 | { 432 | bw.Write(0U); // Initial data chunk length, always 0. 433 | } 434 | 435 | using (new BlockLengthWriter(bw)) 436 | { 437 | int count = this.layerBitmapInfo.Length; 438 | 439 | for (int i = 0; i < count; i++) 440 | { 441 | bw.Write(PSPConstants.blockIdentifier); 442 | bw.Write((ushort)PSPBlockID.Layer); 443 | if (majorVersion <= PSPConstants.majorVersion5) 444 | { 445 | bw.Write(LayerInfoChunk.Version5ChunkLength); // Initial data chunk length. 446 | } 447 | 448 | using (new BlockLengthWriter(bw)) 449 | { 450 | this.layerInfoChunks[i].Save(bw, majorVersion); 451 | this.layerBitmapInfo[i].Save(bw, majorVersion); 452 | } 453 | } 454 | } 455 | } 456 | 457 | private sealed class RasterLayerChunk 458 | { 459 | public readonly LayerInfoChunk layerInfo; 460 | public readonly long bitmapInfoOffset; 461 | 462 | public RasterLayerChunk(LayerInfoChunk info, long offset) 463 | { 464 | this.layerInfo = info; 465 | this.bitmapInfoOffset = offset; 466 | } 467 | } 468 | 469 | private static IList CountRasterChunks(EndianBinaryReader reader, int layerCount, ushort majorVersion) 470 | { 471 | List rasterChunks = new List(layerCount); 472 | 473 | int index = 0; 474 | while (index < layerCount) 475 | { 476 | uint head = reader.ReadUInt32(); 477 | if (head != PSPConstants.blockIdentifier) 478 | { 479 | throw new FormatException(Properties.Resources.InvalidBlockSignature); 480 | } 481 | PSPBlockID blockID = (PSPBlockID)reader.ReadUInt16(); 482 | #pragma warning disable IDE0059 // Value assigned to symbol is never used 483 | uint initialBlockLength = majorVersion <= PSPConstants.majorVersion5 ? reader.ReadUInt32() : 0; 484 | #pragma warning restore IDE0059 // Value assigned to symbol is never used 485 | uint blockLength = reader.ReadUInt32(); 486 | 487 | if (blockID == PSPBlockID.Layer) 488 | { 489 | index++; 490 | long endOffset = reader.Position + blockLength; 491 | 492 | LayerInfoChunk chunk = new LayerInfoChunk(reader, majorVersion); 493 | long currentOffset = reader.Position; 494 | 495 | switch (chunk.type) 496 | { 497 | case PSPLayerType.Raster: 498 | case PSPLayerType.FloatingRasterSelection: 499 | if (majorVersion >= PSPConstants.majorVersion12) 500 | { 501 | // Paint Shop Pro X2 and later insert an unknown block (0x21) before the start of the LayerBitmapInfo chunk. 502 | bool ok = false; 503 | if (reader.ReadUInt32() == PSPConstants.blockIdentifier) 504 | { 505 | ushort block = reader.ReadUInt16(); 506 | uint length = reader.ReadUInt32(); 507 | 508 | if (block == 0x21) 509 | { 510 | reader.Position += length; 511 | if (reader.ReadUInt32() == LayerBitmapInfoChunk.HeaderSize) 512 | { 513 | reader.Position -= 4L; 514 | currentOffset = reader.Position; 515 | ok = true; 516 | } 517 | } 518 | } 519 | 520 | if (!ok) 521 | { 522 | throw new FormatException(Properties.Resources.UnsupportedFormatVersion); 523 | } 524 | } 525 | 526 | rasterChunks.Add(new RasterLayerChunk(chunk, currentOffset)); 527 | break; 528 | } 529 | 530 | reader.Position += (endOffset - currentOffset); 531 | } 532 | else 533 | { 534 | reader.Position += blockLength; 535 | } 536 | } 537 | 538 | return rasterChunks; 539 | } 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /src/PSPSections/PSPConstants.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | namespace PaintShopProFiletype.PSPSections 13 | { 14 | internal static class PSPConstants 15 | { 16 | /// 17 | /// Paint Shop Pro 5 18 | /// 19 | public const ushort majorVersion5 = 3; 20 | /// 21 | /// Paint Shop Pro 6 22 | /// 23 | public const ushort majorVersion6 = 4; 24 | /// 25 | /// Paint Shop Pro 7 26 | /// 27 | public const ushort majorVersion7 = 5; 28 | /// 29 | /// Paint Shop Pro 8 30 | /// 31 | public const ushort majorVersion8 = 6; 32 | /// 33 | /// Paint Shop Pro 9 34 | /// 35 | public const ushort majorVersion9 = 7; 36 | /// 37 | /// Paint Shop Pro 10 38 | /// 39 | public const ushort majorVersion10 = 8; 40 | /// 41 | /// Paint Shop Pro X2 42 | /// 43 | public const ushort majorVersion12 = 10; 44 | 45 | public const ushort v5ThumbnailBlock = 9; 46 | 47 | public const uint fieldIdentifier = 0x004c467e; 48 | public const uint blockIdentifier = 0x004b427e; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/PSPSections/PSPEnums.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | // PSP File Format Specification is Copyright (C) 2000 Jasc Software, Inc. 13 | 14 | using System; 15 | 16 | namespace PaintShopProFiletype.PSPSections 17 | { 18 | /* Block identifiers. 19 | */ 20 | internal enum PSPBlockID 21 | { 22 | ImageAttributes = 0, // General Image Attributes Block (main) 23 | Creator, // Creator Data Block (main) 24 | ColorPalette, // Color Palette Block (main and sub) 25 | LayerStart, // Layer Bank Block (main) 26 | Layer, // Layer Block (sub) 27 | Channel, // Channel Block (sub) 28 | Selection, // Selection Block (main) 29 | AlphaBank, // Alpha Bank Block (main) 30 | AlphaChannel,// Alpha Channel Block (sub) 31 | CompositeImage, // Composite Image Block (sub) 32 | ExtendedData,// Extended Data Block (main) 33 | PictureTube, // Picture Tube Data Block (main) 34 | AdjustmentLayerExtension, // Adjustment Layer Block (sub) 35 | VectorLayerExtension, // Vector Layer Block (sub) 36 | VectorShape, // Vector Shape Block (sub) 37 | PaintStyle, // Paint Style Block (sub) 38 | CompositeImageBank, // Composite Image Bank (main) 39 | CompositeImageAttributes, // Composite Image Attr. (sub) 40 | JPEGImage, // JPEG Image Block (sub) 41 | LineStyle, // Line Style Block (sub) 42 | TableBank, // Table Bank Block (main) 43 | Table, // Table Block (sub) 44 | Paper, // Vector Table Paper Block (sub) 45 | Pattern, // Vector Table Pattern Block (sub) 46 | GroupLayerExtension, // Group Layer Block (sub) 47 | MaskLayerExtension, // Mask Layer Block (sub) 48 | BrushData, // Brush Data Block (main) 49 | } 50 | 51 | /* Bitmap types. 52 | */ 53 | internal enum PSPDIBType 54 | { 55 | Image = 0, // Layer color bitmap 56 | TransparencyMask, // Layer transparency mask bitmap 57 | UserMask, // Layer user mask bitmap 58 | Selection, // Selection mask bitmap 59 | AlphaMask, // Alpha channel mask bitmap 60 | Thumbnail, // Thumbnail bitmap 61 | ThumbnailTransparencyMask, // Thumbnail transparency mask 62 | AdjustmentLayer, // Adjustment layer bitmap 63 | Composite, // Composite image bitmap 64 | CompositeTransparencyMask, // Composite image transparency 65 | Paper, // Paper bitmap 66 | Pattern, // Pattern bitmap 67 | PatternTransparencyMask, // Pattern transparency mask 68 | }; 69 | 70 | /* Type of image in the composite image bank block. 71 | */ 72 | internal enum PSPCompositeImageType 73 | { 74 | Composite = 0, // Composite Image 75 | Thumbnail, // Thumbnail Image 76 | }; 77 | 78 | /* Channel types. 79 | */ 80 | internal enum PSPChannelType 81 | { 82 | Composite = 0, // Channel of single channel bitmap 83 | Red, // Red channel of 24-bit bitmap 84 | Green, // Green channel of 24-bit bitmap 85 | Blue, // Blue channel of 24-bit bitmap 86 | }; 87 | 88 | /* Possible types of compression. 89 | */ 90 | internal enum PSPCompression 91 | { 92 | None = 0, // No compression 93 | RLE, // RLE compression 94 | LZ77, // LZ77 compression 95 | JPEG // JPEG compression (only used by thumbnail and composite image) 96 | }; 97 | 98 | /* Layer types. 99 | */ 100 | internal enum PSPLayerType 101 | { 102 | Undefined = 0, // Undefined layer type 103 | Raster, // Standard raster layer 104 | FloatingRasterSelection, // Floating selection (raster) 105 | Vector, // Vector layer 106 | Adjustment, // Adjustment layer 107 | } 108 | 109 | /* Layer flags. 110 | */ 111 | [Flags] 112 | internal enum PSPLayerProperties 113 | { 114 | None = 0, 115 | Visible = 1, // Layer is visible 116 | MaskPresence = 2 // Layer has a mask 117 | } 118 | 119 | /* Blend modes. 120 | */ 121 | internal enum PSPBlendModes 122 | { 123 | Normal, 124 | Darken, 125 | Lighten, 126 | LegacyHue, 127 | LegacySaturation, 128 | Color, 129 | LegacyLuminosity, 130 | Multiply, 131 | Screen, 132 | Dissolve, 133 | Overlay, 134 | HardLight, 135 | SoftLight, 136 | Difference, 137 | Dodge, 138 | Burn, 139 | Exclusion, 140 | TrueHue, 141 | TrueSaturation, 142 | TrueColor, 143 | TrueLightness, 144 | Adjust = 255, 145 | }; 146 | 147 | /* Possible metrics used to measure resolution. 148 | */ 149 | internal enum ResolutionMetric 150 | { 151 | Undefined = 0, // Metric unknown 152 | Inch, // Resolution is in inches 153 | Centimeter, // Resolution is in centimeters 154 | }; 155 | 156 | /* Creator application identifiers. 157 | */ 158 | internal enum PSPCreatorAppID 159 | { 160 | Unknown = 0, // Creator application unknown 161 | PaintShopPro, // Creator is Paint Shop Pro 162 | } 163 | 164 | /* Creator field types. 165 | */ 166 | internal enum PSPCreatorFieldID 167 | { 168 | Title = 0, // Image document title field 169 | CreateDate, // Creation date field 170 | ModifiedDate, // Modification date field 171 | Artist, // Artist name field 172 | Copyright, // Copyright holder name field 173 | Description, // Image document description field 174 | ApplicationID, // Creating app id field 175 | ApplicationVersion, // Creating app version field 176 | } 177 | 178 | /* Extended data field types. 179 | */ 180 | internal enum PSPExtendedDataID 181 | { 182 | TransparencyIndex = 0, // Transparency index field 183 | Grid, // Image grid information 184 | Guide, // Image guide information 185 | } 186 | 187 | /* Grid units type. 188 | */ 189 | internal enum PSPGridUnitsType 190 | { 191 | Pixels = 0, // Grid units is pixels 192 | Inches, // Grid units is inches 193 | Centimeters // Grid units is centimeters 194 | } 195 | 196 | /* Graphic contents flags. 197 | */ 198 | [Flags] 199 | internal enum PSPGraphicContents : uint 200 | { 201 | None = 0, 202 | // Layer types 203 | RasterLayers = 0x00000001, // At least one raster layer 204 | VectorLayers = 0x00000002, // At least one vector layer 205 | AdjustmentLayers = 0x00000004, // At least one adjust. layer 206 | // Additional attributes 207 | Thumbnail = 0x01000000, // Has a thumbnail 208 | ThumbnailTransparency = 0x02000000, // Thumbnail transp. 209 | Composite = 0x04000000, // Has a composite image 210 | CompositeTransparency = 0x08000000, // Composite transp. 211 | FlatImage = 0x10000000, // Just a background 212 | Selection = 0x20000000, // Has a selection 213 | FloatingSelectionLayer = 0x40000000, // Has float. selection 214 | AlphaChannels = 0x80000000 // Has alpha channel(s) 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /src/PSPSections/PSPFile.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | // Portions of this file has been adapted from: 13 | ///////////////////////////////////////////////////////////////////////////////// 14 | // Paint.NET // 15 | // Copyright (C) dotPDN LLC, Rick Brewster, Tom Jackson, and contributors. // 16 | // Portions Copyright (C) Microsoft Corporation. All Rights Reserved. // 17 | // See src/Resources/Files/License.txt for full licensing and attribution // 18 | // details. // 19 | // . // 20 | ///////////////////////////////////////////////////////////////////////////////// 21 | 22 | using CommunityToolkit.HighPerformance; 23 | using CommunityToolkit.HighPerformance.Buffers; 24 | using System; 25 | using System.Drawing; 26 | using System.IO; 27 | using System.IO.Compression; 28 | using System.Runtime.CompilerServices; 29 | using PaintDotNet; 30 | using PaintDotNet.ComponentModel; 31 | using PaintDotNet.Imaging; 32 | using PaintDotNet.Rendering; 33 | using PaintShopProFiletype.IO; 34 | using PaintShopProFiletype.PSPSections; 35 | 36 | namespace PaintShopProFiletype 37 | { 38 | internal sealed class PSPFile 39 | { 40 | private static ReadOnlySpan PSPFileSig => new byte[32] 41 | { 0x50, 0x61, 0x69, 0x6E, 0x74, 0x20, 0x53, 0x68, 0x6F, 0x70, 0x20, 0x50, 42 | 0x72, 0x6F, 0x20, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x20, 0x46, 0x69, 0x6C, 0x65, 0x0A, 43 | 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00}; // "Paint Shop Pro Image File\n\x1a” padded to 32 bytes 44 | 45 | private FileHeader? fileHeader; 46 | private GeneralImageAttributes? imageAttributes; 47 | private ExtendedDataBlock? extData; 48 | private CreatorBlock? creator; 49 | private CompositeImageBlock? compImage; 50 | private ColorPaletteBlock? globalPalette; 51 | private LayerBlock? layerBlock; 52 | private ThumbnailBlock? v5Thumbnail; 53 | 54 | private readonly IServiceProvider serviceProvider; 55 | 56 | public PSPFile(IServiceProvider serviceProvider) 57 | { 58 | ArgumentNullException.ThrowIfNull(serviceProvider, nameof(serviceProvider)); 59 | 60 | this.fileHeader = null; 61 | this.imageAttributes = null; 62 | this.extData = null; 63 | this.creator = null; 64 | this.compImage = null; 65 | this.globalPalette = null; 66 | this.layerBlock = null; 67 | this.v5Thumbnail = null; 68 | this.serviceProvider = serviceProvider; 69 | } 70 | 71 | [SkipLocalsInit] 72 | private static bool CheckFileSignature(EndianBinaryReader reader) 73 | { 74 | Span signature = stackalloc byte[32]; 75 | 76 | reader.ReadExactly(signature); 77 | 78 | // Some writers may not write zeros for the signature padding, so we only check the first 27 bytes of the signature. 79 | const int SignatureCheckLength = 27; 80 | 81 | return signature.Slice(0, SignatureCheckLength).SequenceEqual(PSPFileSig.Slice(0, SignatureCheckLength)); 82 | } 83 | 84 | private static LayerBlendMode ConvertFromPSPBlendMode(PSPBlendModes mode) 85 | { 86 | return mode switch 87 | { 88 | PSPBlendModes.Normal => LayerBlendMode.Normal, 89 | PSPBlendModes.Darken => LayerBlendMode.Darken, 90 | PSPBlendModes.Lighten => LayerBlendMode.Lighten, 91 | PSPBlendModes.Multiply => LayerBlendMode.Multiply, 92 | PSPBlendModes.Screen => LayerBlendMode.Screen, 93 | PSPBlendModes.Overlay => LayerBlendMode.Overlay, 94 | PSPBlendModes.Difference => LayerBlendMode.Difference, 95 | PSPBlendModes.Dodge => LayerBlendMode.ColorDodge, 96 | PSPBlendModes.Burn => LayerBlendMode.ColorBurn, 97 | _ => LayerBlendMode.Normal, 98 | }; 99 | } 100 | 101 | private void LoadPSPFile(Stream input) 102 | { 103 | using (EndianBinaryReader reader = new EndianBinaryReader(input, Endianess.Little, leaveOpen: true)) 104 | { 105 | if (!CheckFileSignature(reader)) 106 | { 107 | throw new FormatException(Properties.Resources.InvalidPSPFile); 108 | } 109 | 110 | this.fileHeader = new FileHeader(reader); 111 | 112 | while (reader.Position < reader.Length) 113 | { 114 | uint blockSig = reader.ReadUInt32(); 115 | if (blockSig != PSPConstants.blockIdentifier) 116 | { 117 | throw new FormatException(Properties.Resources.InvalidBlockSignature); 118 | } 119 | PSPBlockID blockID = (PSPBlockID)reader.ReadUInt16(); 120 | uint initialBlockLength = this.fileHeader.Major <= PSPConstants.majorVersion5 ? reader.ReadUInt32() : 0; 121 | uint blockLength = reader.ReadUInt32(); 122 | 123 | switch (blockID) 124 | { 125 | case PSPBlockID.ImageAttributes: 126 | this.imageAttributes = new GeneralImageAttributes(reader, this.fileHeader.Major); 127 | break; 128 | case PSPBlockID.Creator: 129 | this.creator = new CreatorBlock(reader, blockLength); 130 | break; 131 | case PSPBlockID.ColorPalette: 132 | this.globalPalette = new ColorPaletteBlock(reader, this.fileHeader.Major); 133 | break; 134 | case PSPBlockID.LayerStart: 135 | this.layerBlock = new LayerBlock(reader, this.imageAttributes, this.fileHeader.Major); 136 | break; 137 | case PSPBlockID.ExtendedData: 138 | this.extData = new ExtendedDataBlock(reader, blockLength); 139 | break; 140 | #if DEBUG 141 | case PSPBlockID.CompositeImageBank: 142 | this.compImage = new CompositeImageBlock(reader, this.fileHeader.Major); 143 | break; 144 | #endif 145 | default: 146 | reader.Position += blockLength; 147 | break; 148 | } 149 | } 150 | } 151 | } 152 | 153 | private static unsafe byte[] ExpandPackedPalette(Rectangle saveRect, LayerBitmapInfoChunk bitmap, int bitDepth) 154 | { 155 | int height = saveRect.Height; 156 | int width = saveRect.Width; 157 | byte[] image = new byte[width * height]; 158 | 159 | int bpp, shift; 160 | 161 | switch (bitDepth) 162 | { 163 | case 4: 164 | bpp = 2; 165 | shift = 1; 166 | break; 167 | case 1: 168 | bpp = 8; 169 | shift = 3; 170 | break; 171 | default: 172 | throw new ArgumentOutOfRangeException(nameof(bitDepth), string.Format("Bit depth value of {0} is not supported, value must be 1 or 4.", bitDepth)); 173 | } 174 | 175 | fixed (byte* ptr = image, dPtr = bitmap.channels![0].channelData) 176 | { 177 | int srcStride = width / bpp; 178 | 179 | for (int y = 0; y < height; y++) 180 | { 181 | byte* src = dPtr + (y * srcStride); 182 | byte* dst = ptr + (y * width); 183 | 184 | for (int x = 0; x < width; x++) 185 | { 186 | byte data = src[x >> shift]; 187 | 188 | switch (bitDepth) 189 | { 190 | case 4: 191 | 192 | if ((x & 1) == 1) // odd column 193 | { 194 | *dst = (byte)(data & 0x0f); 195 | } 196 | else 197 | { 198 | *dst = (byte)(data >> 4); 199 | } 200 | 201 | break; 202 | case 1: 203 | 204 | int mask = x & 7; 205 | // extract the palette bit for the current pixel. 206 | *dst = (byte)((data & (128 >> mask)) >> (7 - mask)); 207 | break; 208 | } 209 | 210 | dst++; 211 | } 212 | } 213 | } 214 | 215 | return image; 216 | } 217 | 218 | public Document Load(Stream input) 219 | { 220 | LoadPSPFile(input); 221 | 222 | if (this.imageAttributes == null || this.layerBlock == null) 223 | { 224 | throw new FormatException(Properties.Resources.InvalidPSPFile); 225 | } 226 | 227 | if (this.imageAttributes.BitDepth <= 8 && this.globalPalette == null) 228 | { 229 | throw new FormatException(Properties.Resources.ColorPaletteNotFound); 230 | } 231 | 232 | LayerInfoChunk[] infoChunks = this.layerBlock.LayerInfo; 233 | int layerCount = infoChunks.Length; 234 | 235 | // Some PSP files may have layers that are larger than the dimensions stored in the image attributes. 236 | int maxWidth = this.imageAttributes.Width; 237 | int maxHeight = this.imageAttributes.Height; 238 | 239 | for (int i = 0; i < layerCount; i++) 240 | { 241 | Rectangle savedBounds = infoChunks[i].saveRect; 242 | if (savedBounds.Width > maxWidth) 243 | { 244 | maxWidth = savedBounds.Width; 245 | } 246 | if (savedBounds.Height > maxHeight) 247 | { 248 | maxHeight = savedBounds.Height; 249 | } 250 | } 251 | 252 | if (maxWidth <= 0 || maxHeight <= 0) 253 | { 254 | throw new FormatException(Properties.Resources.InvalidDocumentDimensions); 255 | } 256 | 257 | Document doc = new Document(maxWidth, maxHeight); 258 | 259 | if (this.imageAttributes.ResValue > 0.0) 260 | { 261 | switch (this.imageAttributes.ResUnit) 262 | { 263 | case ResolutionMetric.Inch: 264 | doc.DpuUnit = MeasurementUnit.Inch; 265 | doc.DpuX = doc.DpuY = this.imageAttributes.ResValue; 266 | break; 267 | case ResolutionMetric.Centimeter: 268 | doc.DpuUnit = MeasurementUnit.Centimeter; 269 | doc.DpuX = doc.DpuY = this.imageAttributes.ResValue; 270 | break; 271 | default: 272 | break; 273 | } 274 | } 275 | 276 | short transIndex = -1; 277 | 278 | if (this.imageAttributes.BitDepth < 24 && this.extData != null) 279 | { 280 | transIndex = this.extData.TryGetTransparencyIndex(); 281 | } 282 | 283 | LayerBitmapInfoChunk[] bitmapInfoChunks = this.layerBlock.LayerBitmapInfo; 284 | 285 | for (int i = 0; i < layerCount; i++) 286 | { 287 | LayerInfoChunk info = infoChunks[i]; 288 | 289 | BitmapLayer layer = new BitmapLayer(doc.Width, doc.Height) 290 | { 291 | Name = info.name, 292 | Opacity = info.opacity, 293 | BlendMode = ConvertFromPSPBlendMode(info.blendMode), 294 | Visible = (info.layerFlags & PSPLayerProperties.Visible) == PSPLayerProperties.Visible 295 | }; 296 | 297 | Rectangle saveRect = info.saveRect; 298 | 299 | if (!saveRect.IsEmpty) 300 | { 301 | LayerBitmapInfoChunk bitmapInfo = bitmapInfoChunks[i]; 302 | 303 | if (bitmapInfo.bitmapCount == 1) 304 | { 305 | new UnaryPixelOps.SetAlphaChannelTo255().Apply(layer.Surface, saveRect); 306 | } 307 | 308 | int alphaIndex = 0; 309 | 310 | int bitDepth = this.imageAttributes.BitDepth; 311 | 312 | int bytesPerPixel = 1; 313 | int stride = saveRect.Width; 314 | byte[]? expandedPalette = null; 315 | 316 | switch (bitDepth) 317 | { 318 | case 48: 319 | bytesPerPixel = 2; 320 | stride *= 2; 321 | break; 322 | case 4: 323 | case 1: 324 | expandedPalette = ExpandPackedPalette(saveRect, bitmapInfo, bitDepth); 325 | break; 326 | } 327 | 328 | unsafe 329 | { 330 | int palIdx = 0; 331 | RGBQUAD entry; 332 | 333 | Surface surface = layer.Surface; 334 | for (int y = saveRect.Top; y < saveRect.Bottom; y++) 335 | { 336 | ColorBgra* ptr = surface.GetPointPointerUnchecked(saveRect.Left, y); 337 | ColorBgra* endPtr = ptr + saveRect.Width; 338 | int index = ((y - saveRect.Top) * stride); 339 | 340 | while (ptr < endPtr) 341 | { 342 | switch (bitDepth) 343 | { 344 | case 48: 345 | 346 | for (int ci = 0; ci < bitmapInfo.channelCount; ci++) 347 | { 348 | ChannelSubBlock ch = bitmapInfo.channels[ci]; 349 | 350 | if (ch.bitmapType == PSPDIBType.Image) 351 | { 352 | ushort col = (ushort)(ch.channelData[index] | (ch.channelData[index + 1] << 8)); // PSP format is always little endian 353 | byte clamped = (byte)((col * 255) / 65535); 354 | 355 | switch (ch.channelType) 356 | { 357 | case PSPChannelType.Red: 358 | ptr->R = clamped; 359 | break; 360 | case PSPChannelType.Green: 361 | ptr->G = clamped; 362 | break; 363 | case PSPChannelType.Blue: 364 | ptr->B = clamped; 365 | break; 366 | } 367 | } 368 | else if (ch.bitmapType == PSPDIBType.TransparencyMask) 369 | { 370 | ptr->A = ch.channelData[alphaIndex]; 371 | alphaIndex++; 372 | } 373 | } 374 | 375 | break; 376 | case 24: 377 | 378 | for (int ci = 0; ci < bitmapInfo.channelCount; ci++) 379 | { 380 | ChannelSubBlock ch = bitmapInfo.channels[ci]; 381 | 382 | if (ch.bitmapType == PSPDIBType.Image) 383 | { 384 | switch (ch.channelType) 385 | { 386 | case PSPChannelType.Red: 387 | ptr->R = ch.channelData[index]; 388 | break; 389 | case PSPChannelType.Green: 390 | ptr->G = ch.channelData[index]; 391 | break; 392 | case PSPChannelType.Blue: 393 | ptr->B = ch.channelData[index]; 394 | break; 395 | } 396 | } 397 | else if (ch.bitmapType == PSPDIBType.TransparencyMask) 398 | { 399 | ptr->A = ch.channelData[index]; 400 | } 401 | } 402 | 403 | break; 404 | case 8: 405 | for (int ci = 0; ci < bitmapInfo.channelCount; ci++) 406 | { 407 | ChannelSubBlock ch = bitmapInfo.channels[ci]; 408 | palIdx = ch.channelData[index]; 409 | entry = this.globalPalette!.entries[palIdx]; 410 | 411 | switch (ch.bitmapType) 412 | { 413 | case PSPDIBType.Image: 414 | ptr->R = entry.red; 415 | ptr->G = entry.green; 416 | ptr->B = entry.blue; 417 | 418 | if (palIdx == transIndex) 419 | { 420 | ptr->A = 0; 421 | } 422 | 423 | break; 424 | case PSPDIBType.TransparencyMask: 425 | ptr->A = entry.red; 426 | break; 427 | } 428 | } 429 | 430 | break; 431 | case 4: 432 | case 1: 433 | 434 | palIdx = expandedPalette![index]; 435 | entry = this.globalPalette!.entries[palIdx]; 436 | 437 | ptr->R = entry.red; 438 | ptr->G = entry.green; 439 | ptr->B = entry.blue; 440 | 441 | if (palIdx == transIndex) 442 | { 443 | ptr->A = 0; 444 | } 445 | else if ((bitmapInfo.bitmapCount == 2) && bitmapInfo.channels[1].bitmapType == PSPDIBType.TransparencyMask) 446 | { 447 | ptr->A = bitmapInfo.channels[1].channelData[index]; 448 | } 449 | 450 | break; 451 | default: 452 | throw new FormatException(string.Format(Properties.Resources.UnsupportedBitDepth, bitDepth)); 453 | 454 | } 455 | 456 | ptr++; 457 | index += bytesPerPixel; 458 | } 459 | } 460 | } 461 | } 462 | 463 | #if DEBUG 464 | using (Bitmap temp = layer.Surface.CreateAliasedBitmap()) 465 | { 466 | } 467 | #endif 468 | doc.Layers.Add(layer); 469 | } 470 | 471 | CreatorBlockSerialization.Serialize(this.creator, doc); 472 | 473 | return doc; 474 | } 475 | 476 | private static PSPBlendModes ConvertToPSPBlendMode(LayerBlendMode blendMode) 477 | { 478 | return blendMode switch 479 | { 480 | LayerBlendMode.Normal => PSPBlendModes.Normal, 481 | LayerBlendMode.Multiply => PSPBlendModes.Multiply, 482 | LayerBlendMode.ColorBurn => PSPBlendModes.Burn, 483 | LayerBlendMode.ColorDodge => PSPBlendModes.Dodge, 484 | LayerBlendMode.Overlay => PSPBlendModes.Overlay, 485 | LayerBlendMode.Difference => PSPBlendModes.Difference, 486 | LayerBlendMode.Lighten => PSPBlendModes.Lighten, 487 | LayerBlendMode.Darken => PSPBlendModes.Darken, 488 | LayerBlendMode.Screen => PSPBlendModes.Screen, 489 | _ => PSPBlendModes.Normal, 490 | }; 491 | } 492 | 493 | private static PSPCompression CompressionFromTokenFormat(CompressionFormats format) 494 | { 495 | PSPCompression comp = PSPCompression.None; 496 | 497 | switch (format) 498 | { 499 | case CompressionFormats.LZ77: 500 | comp = PSPCompression.LZ77; 501 | break; 502 | } 503 | 504 | return comp; 505 | } 506 | 507 | private int doneProgress; 508 | private int totalProgress; 509 | private unsafe ChannelSubBlock[] SplitImageChannels( 510 | Surface source, 511 | Rectangle savedBounds, 512 | int channelCount, 513 | ushort majorVersion, 514 | bool composite, 515 | ProgressEventHandler callback) 516 | { 517 | ChannelSubBlock[] channels = new ChannelSubBlock[channelCount]; 518 | 519 | int channelSize = checked(savedBounds.Width * savedBounds.Height); 520 | 521 | for (int i = 0; i < channelCount; i++) 522 | { 523 | channels[i] = new ChannelSubBlock(majorVersion, (uint)channelSize); 524 | if (composite) 525 | { 526 | channels[i].bitmapType = majorVersion switch 527 | { 528 | PSPConstants.majorVersion5 => PSPDIBType.Thumbnail, 529 | _ => i < 3 ? PSPDIBType.Composite : PSPDIBType.CompositeTransparencyMask, 530 | }; 531 | } 532 | else 533 | { 534 | channels[i].bitmapType = i < 3 ? PSPDIBType.Image : PSPDIBType.TransparencyMask; 535 | } 536 | 537 | switch (i) 538 | { 539 | case 0: 540 | channels[i].channelType = PSPChannelType.Red; 541 | break; 542 | case 1: 543 | channels[i].channelType = PSPChannelType.Green; 544 | break; 545 | case 2: 546 | channels[i].channelType = PSPChannelType.Blue; 547 | break; 548 | case 3: 549 | channels[i].channelType = PSPChannelType.Composite; 550 | break; 551 | } 552 | } 553 | 554 | if (channelSize > 0) 555 | { 556 | RegionPtr sourceRegion = source.AsRegionPtr().Slice(savedBounds).Cast(); 557 | 558 | using (SpanOwner uncompressedDataOwner = SpanOwner.Allocate(channelSize)) 559 | { 560 | Span uncompresedData = uncompressedDataOwner.Span; 561 | 562 | fixed (byte* buffer = uncompresedData) 563 | { 564 | RegionPtr targetRegion = new RegionPtr(buffer, savedBounds.Size, savedBounds.Width); 565 | 566 | for (int channelIndex = 0; channelIndex < channelCount; channelIndex++) 567 | { 568 | int sourceIndex = channelIndex; 569 | 570 | // Map BGRA to RGBA 571 | switch (channelIndex) 572 | { 573 | case 0: 574 | sourceIndex = 2; 575 | break; 576 | case 2: 577 | sourceIndex = 0; 578 | break; 579 | } 580 | 581 | PixelKernels.ExtractChannel(targetRegion, sourceRegion, sourceIndex); 582 | 583 | ChannelSubBlock channel = channels[channelIndex]; 584 | 585 | switch (this.imageAttributes!.CompressionType) 586 | { 587 | case PSPCompression.None: 588 | channel.compressedChannelLength = (uint)channelSize; 589 | channel.uncompressedChannelLength = 0; 590 | channel.channelData = uncompresedData.ToArray(); 591 | break; 592 | case PSPCompression.LZ77: 593 | 594 | using (ArrayPoolBufferWriter bufferWriter = new ArrayPoolBufferWriter()) 595 | { 596 | using (ZLibStream zs = new ZLibStream(bufferWriter.AsStream(), CompressionLevel.SmallestSize, true)) 597 | { 598 | zs.Write(uncompresedData); 599 | } 600 | 601 | ReadOnlySpan compressedData = bufferWriter.WrittenSpan; 602 | channel.compressedChannelLength = (uint)compressedData.Length; 603 | channel.uncompressedChannelLength = (uint)channelSize; 604 | channel.channelData = compressedData.ToArray(); 605 | } 606 | break; 607 | } 608 | 609 | if (callback != null) 610 | { 611 | this.doneProgress++; 612 | 613 | callback(this, new ProgressEventArgs(100.0 * ((double)this.doneProgress / this.totalProgress))); 614 | } 615 | } 616 | } 617 | } 618 | } 619 | 620 | return channels; 621 | } 622 | 623 | private static Size GetThumbnailDimensions(int originalWidth, int originalHeight, int maxEdgeLength) 624 | { 625 | Size thumbSize = Size.Empty; 626 | 627 | if (originalWidth <= 0 || originalHeight <= 0) 628 | { 629 | thumbSize.Width = 1; 630 | thumbSize.Height = 1; 631 | } 632 | else if (originalWidth > originalHeight) 633 | { 634 | int longSide = Math.Min(originalWidth, maxEdgeLength); 635 | thumbSize.Width = longSide; 636 | thumbSize.Height = Math.Max(1, (originalHeight * longSide) / originalWidth); 637 | } 638 | else if (originalHeight > originalWidth) 639 | { 640 | int longSide = Math.Min(originalHeight, maxEdgeLength); 641 | thumbSize.Width = Math.Max(1, (originalWidth * longSide) / originalHeight); 642 | thumbSize.Height = longSide; 643 | } 644 | else 645 | { 646 | int longSide = Math.Min(originalWidth, maxEdgeLength); 647 | thumbSize.Width = longSide; 648 | thumbSize.Height = longSide; 649 | } 650 | 651 | return thumbSize; 652 | } 653 | 654 | private static unsafe bool LayerHasTransparency(Surface surface, Rectangle bounds) 655 | { 656 | for (int y = bounds.Top; y < bounds.Bottom; y++) 657 | { 658 | ColorBgra* startPtr = surface.GetPointPointerUnchecked(bounds.Left, y); 659 | ColorBgra* endPtr = startPtr + bounds.Width; 660 | 661 | while (startPtr < endPtr) 662 | { 663 | if (startPtr->A < 255) 664 | { 665 | return true; 666 | } 667 | 668 | startPtr++; 669 | } 670 | } 671 | 672 | return false; 673 | } 674 | 675 | public void Save(Document input, Stream output, CompressionFormats format, Surface scratchSurface, ushort majorVersion, ProgressEventHandler callback) 676 | { 677 | if (majorVersion == PSPConstants.majorVersion5 && input.Layers.Count > 64) 678 | { 679 | throw new FormatException(string.Format(Properties.Resources.MaxLayersFormat, 64)); 680 | } 681 | else if ((majorVersion == PSPConstants.majorVersion6 || majorVersion == PSPConstants.majorVersion7) && input.Layers.Count > 100) 682 | { 683 | throw new FormatException(string.Format(Properties.Resources.MaxLayersFormat, 100)); 684 | } 685 | 686 | using (BinaryWriterEx writer = new BinaryWriterEx(output, false)) 687 | { 688 | this.fileHeader = new FileHeader(majorVersion); 689 | this.imageAttributes = new GeneralImageAttributes( 690 | input.Width, 691 | input.Height, 692 | CompressionFromTokenFormat(format), 693 | 0, 694 | input.Layers.Count, 695 | majorVersion); 696 | switch (input.DpuUnit) 697 | { 698 | case MeasurementUnit.Centimeter: 699 | this.imageAttributes.ResValue = input.DpuX; 700 | this.imageAttributes.ResUnit = ResolutionMetric.Centimeter; 701 | break; 702 | case MeasurementUnit.Inch: 703 | this.imageAttributes.ResValue = input.DpuX; 704 | this.imageAttributes.ResUnit = ResolutionMetric.Inch; 705 | break; 706 | } 707 | 708 | this.totalProgress = 0; 709 | this.doneProgress = 0; 710 | 711 | bool flatImage = true; 712 | foreach (Layer item in input.Layers) 713 | { 714 | BitmapLayer layer = (BitmapLayer)item; 715 | 716 | Rectangle rect = PSPUtil.GetImageSaveRectangle(layer.Surface); 717 | 718 | if (!rect.IsEmpty) 719 | { 720 | if (LayerHasTransparency(layer.Surface, rect)) 721 | { 722 | this.totalProgress += 4; 723 | flatImage = false; 724 | } 725 | else 726 | { 727 | this.totalProgress += 3; 728 | } 729 | } 730 | 731 | } 732 | 733 | if (flatImage) 734 | { 735 | this.imageAttributes.SetGraphicContentFlag(PSPGraphicContents.FlatImage); 736 | } 737 | 738 | if (majorVersion > PSPConstants.majorVersion5) 739 | { 740 | CreateCompositeImageBlock(input, scratchSurface, callback, majorVersion); 741 | } 742 | else 743 | { 744 | CreateThumbnailBlock(input, scratchSurface, callback, majorVersion); 745 | } 746 | 747 | int layerCount = input.Layers.Count; 748 | 749 | LayerInfoChunk[] layerInfoChunks = new LayerInfoChunk[layerCount]; 750 | LayerBitmapInfoChunk[] layerBitmapChunks = new LayerBitmapInfoChunk[layerCount]; 751 | 752 | for (int i = 0; i < layerCount; i++) 753 | { 754 | BitmapLayer layer = (BitmapLayer)input.Layers[i]; 755 | 756 | Rectangle savedBounds = PSPUtil.GetImageSaveRectangle(layer.Surface); 757 | 758 | LayerInfoChunk infoChunk = new LayerInfoChunk(layer, ConvertToPSPBlendMode(layer.BlendMode), savedBounds, majorVersion); 759 | 760 | int channelCount = 3; 761 | int bitmapCount = 1; 762 | 763 | if (LayerHasTransparency(layer.Surface, savedBounds)) 764 | { 765 | channelCount = 4; 766 | bitmapCount = 2; 767 | } 768 | 769 | LayerBitmapInfoChunk biChunk = new LayerBitmapInfoChunk(bitmapCount, SplitImageChannels(layer.Surface, 770 | savedBounds, 771 | channelCount, 772 | majorVersion, 773 | false, 774 | callback)); 775 | 776 | if (majorVersion <= PSPConstants.majorVersion5) 777 | { 778 | infoChunk.v5BitmapCount = biChunk.bitmapCount; 779 | infoChunk.v5ChannelCount = biChunk.channelCount; 780 | } 781 | 782 | layerInfoChunks[i] = infoChunk; 783 | layerBitmapChunks[i] = biChunk; 784 | } 785 | 786 | this.layerBlock = new LayerBlock(layerInfoChunks, layerBitmapChunks); 787 | this.creator = CreatorBlockSerialization.Deserialize(input); 788 | 789 | writer.Write(PSPFileSig); 790 | this.fileHeader.Save(writer); 791 | this.imageAttributes.Save(writer); 792 | this.creator?.Save(writer, majorVersion); 793 | if (this.compImage != null) 794 | { 795 | this.compImage.Save(writer); 796 | } 797 | else 798 | { 799 | this.v5Thumbnail?.Save(writer); 800 | } 801 | this.layerBlock.Save(writer, majorVersion); 802 | } 803 | } 804 | 805 | private void CreateCompositeImageBlock(Document input, Surface scratchSurface, ProgressEventHandler callback, ushort majorVersion) 806 | { 807 | Size jpegThumbSize = GetThumbnailDimensions(input.Width, input.Height, 200); 808 | 809 | CompositeImageAttributesChunk normAttr = new CompositeImageAttributesChunk( 810 | input.Width, 811 | input.Height, 812 | PSPCompositeImageType.Composite, 813 | this.imageAttributes!.CompressionType); 814 | CompositeImageAttributesChunk jpgAttr = new CompositeImageAttributesChunk( 815 | jpegThumbSize.Width, 816 | jpegThumbSize.Height, 817 | PSPCompositeImageType.Thumbnail, 818 | PSPCompression.JPEG); 819 | JPEGCompositeInfoChunk jpgChunk = new JPEGCompositeInfoChunk(); 820 | CompositeImageInfoChunk infoChunk = new CompositeImageInfoChunk(); 821 | 822 | scratchSurface.Fill(ColorBgra.TransparentBlack); 823 | input.CreateRenderer().Render(scratchSurface); 824 | 825 | int channelCount = 3; 826 | if (LayerHasTransparency(scratchSurface, scratchSurface.Bounds)) 827 | { 828 | channelCount = 4; 829 | infoChunk.channelCount = 4; 830 | infoChunk.bitmapCount = 2; 831 | } 832 | #if DEBUG 833 | using (Bitmap bmp = scratchSurface.CreateAliasedBitmap()) 834 | { 835 | } 836 | #endif 837 | 838 | this.totalProgress += channelCount; 839 | 840 | infoChunk.channelBlocks = SplitImageChannels(scratchSurface, scratchSurface.Bounds, channelCount, majorVersion, true, callback); 841 | 842 | jpgChunk.imageData = GetJpegThumbnailData(jpegThumbSize, scratchSurface, channelCount); 843 | jpgChunk.compressedSize = (uint)jpgChunk.imageData.Length; 844 | 845 | this.imageAttributes.SetGraphicContentFlag(PSPGraphicContents.Composite); 846 | this.imageAttributes.SetGraphicContentFlag(PSPGraphicContents.Thumbnail); 847 | 848 | if (infoChunk.bitmapCount == 2) 849 | { 850 | this.imageAttributes.SetGraphicContentFlag(PSPGraphicContents.CompositeTransparency); 851 | } 852 | 853 | this.compImage = new CompositeImageBlock(new CompositeImageAttributesChunk[2] { jpgAttr, normAttr }, jpgChunk, infoChunk); 854 | } 855 | 856 | private void CreateThumbnailBlock(Document input, Surface scratchSurface, ProgressEventHandler callback, ushort majorVersion) 857 | { 858 | Size thumbSize = GetThumbnailDimensions(input.Width, input.Height, 300); 859 | 860 | scratchSurface.Fill(ColorBgra.White); 861 | input.CreateRenderer().Render(scratchSurface); 862 | 863 | using (Surface fit = new Surface(thumbSize)) 864 | { 865 | fit.FitSurface(ResamplingAlgorithm.SuperSampling, scratchSurface); 866 | 867 | this.totalProgress += 3; 868 | 869 | ChannelSubBlock[] channels = SplitImageChannels(scratchSurface, scratchSurface.Bounds, 3, majorVersion, true, callback); 870 | this.v5Thumbnail = new ThumbnailBlock(thumbSize.Width, thumbSize.Height, channels); 871 | } 872 | } 873 | 874 | private byte[] GetJpegThumbnailData(Size jpegThumbSize, Surface scratchSurface, int channelCount) 875 | { 876 | IImagingFactory? imagingFactory = this.serviceProvider.GetService(); 877 | 878 | #pragma warning disable IDE0270 // Use coalesce expression 879 | if (imagingFactory is null) 880 | { 881 | throw new InvalidOperationException($"Failed to get the {nameof(IImagingFactory)} for saving the image thumbnail."); 882 | } 883 | #pragma warning restore IDE0270 // Use coalesce expression 884 | 885 | int width = jpegThumbSize.Width; 886 | int height = jpegThumbSize.Height; 887 | 888 | byte[] jpegThumbnailData; 889 | 890 | using (Surface fit = new Surface(width, height)) 891 | { 892 | fit.FitSurface(ResamplingAlgorithm.AdaptiveHighQuality, scratchSurface); 893 | 894 | if (channelCount == 4) 895 | { 896 | unsafe 897 | { 898 | for (int y = 0; y < height; y++) 899 | { 900 | ColorBgra* ptr = fit.GetRowPointerUnchecked(y); 901 | ColorBgra* endPtr = ptr + width; 902 | 903 | while (ptr < endPtr) 904 | { 905 | if (ptr->A == 0) 906 | { 907 | ptr->Bgra |= 0x00ffffff; // set the color of the transparent pixels to white, same as Paint Shop Pro 908 | } 909 | 910 | ptr++; 911 | } 912 | } 913 | } 914 | } 915 | 916 | using (MemoryStream stream = new MemoryStream()) 917 | { 918 | using (IBitmap sharedBitmap = fit.CreateSharedBitmap()) 919 | using (IBitmapSource convertedBitmap = sharedBitmap.CreateFormatConverter()) 920 | using (IBitmapEncoder encoder = imagingFactory.CreateEncoder(stream, ContainerFormats.Jpeg)) 921 | using (IBitmapFrameEncode frameEncode = encoder.CreateNewFrame(out IPropertyBag2 encoderOptions)) 922 | { 923 | frameEncode.Initialize(encoderOptions); 924 | 925 | frameEncode.SetSize(width, height); 926 | frameEncode.SetPixelFormat(PixelFormats.Bgr24); 927 | frameEncode.WriteSource(convertedBitmap); 928 | 929 | frameEncode.Commit(); 930 | encoder.Commit(); 931 | } 932 | 933 | jpegThumbnailData = stream.ToArray(); 934 | } 935 | } 936 | 937 | return jpegThumbnailData; 938 | } 939 | } 940 | } 941 | -------------------------------------------------------------------------------- /src/PSPSections/PSPUtil.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | // Portions of this file has been adapted from: 13 | ///////////////////////////////////////////////////////////////////////////////// 14 | // 15 | // Photoshop PSD FileType Plugin for Paint.NET 16 | // http://psdplugin.codeplex.com/ 17 | // 18 | // This software is provided under the MIT License: 19 | // Copyright (c) 2006-2007 Frank Blumenberg 20 | // Copyright (c) 2010-2013 Tao Yue 21 | // 22 | // See LICENSE.txt for complete licensing and attribution information. 23 | // 24 | ///////////////////////////////////////////////////////////////////////////////// 25 | 26 | using System; 27 | using System.Drawing; 28 | using System.Globalization; 29 | using PaintDotNet; 30 | 31 | namespace PaintShopProFiletype.PSPSections 32 | { 33 | internal static partial class PSPUtil 34 | { 35 | [System.Diagnostics.DebuggerDisplay("Top = {Top}, Bottom = {Bottom}, Left = {Left}, Right = {Right}")] 36 | private struct RectanglePosition 37 | { 38 | public int Top { get; set; } 39 | public int Bottom { get; set; } 40 | public int Left { get; set; } 41 | public int Right { get; set; } 42 | } 43 | 44 | public static unsafe Rectangle GetImageSaveRectangle(Surface surface) 45 | { 46 | RectanglePosition rectPos = new RectanglePosition() 47 | { 48 | Left = surface.Width, 49 | Top = surface.Height, 50 | Right = 0, 51 | Bottom = 0 52 | }; 53 | 54 | // Search for top non-transparent pixel 55 | bool fPixelFound = false; 56 | for (int y = 0; y < surface.Height; y++) 57 | { 58 | if (ExpandImageRectangle(surface, y, 0, surface.Width, ref rectPos)) 59 | { 60 | fPixelFound = true; 61 | break; 62 | } 63 | } 64 | 65 | // Narrow down the other dimensions of the image rectangle 66 | if (fPixelFound) 67 | { 68 | // Search for bottom non-transparent pixel 69 | for (int y = surface.Height - 1; y > rectPos.Bottom; y--) 70 | { 71 | if (ExpandImageRectangle(surface, y, 0, surface.Width, ref rectPos)) 72 | { 73 | break; 74 | } 75 | } 76 | 77 | // Search for left and right non-transparent pixels. Because we 78 | // scan horizontally, we can't just break, but we can examine fewer 79 | // candidate pixels on the remaining rows. 80 | for (int y = rectPos.Top + 1; y < rectPos.Bottom; y++) 81 | { 82 | ExpandImageRectangle(surface, y, 0, rectPos.Left, ref rectPos); 83 | ExpandImageRectangle(surface, y, rectPos.Right + 1, surface.Width, ref rectPos); 84 | } 85 | } 86 | else 87 | { 88 | rectPos.Left = 0; 89 | rectPos.Top = 0; 90 | } 91 | 92 | if ((rectPos.Left < rectPos.Right) && (rectPos.Top < rectPos.Bottom)) 93 | { 94 | return new Rectangle(rectPos.Left, rectPos.Top, rectPos.Right - rectPos.Left + 1, rectPos.Bottom - rectPos.Top + 1); 95 | } 96 | 97 | return Rectangle.Empty; 98 | } 99 | 100 | /// 101 | /// Check for non-transparent pixels in a row, or portion of a row. 102 | /// Expands the size of the image rectangle if any were found. 103 | /// 104 | /// True if non-transparent pixels were found, false otherwise. 105 | unsafe private static bool ExpandImageRectangle(Surface surface, int y, 106 | int xStart, int xEnd, ref RectanglePosition rectPos) 107 | { 108 | bool fPixelFound = false; 109 | 110 | ColorBgra* rowStart = surface.GetRowPointer(y); 111 | ColorBgra* pixel = rowStart + xStart; 112 | for (int x = xStart; x < xEnd; x++) 113 | { 114 | if (pixel->A > 0) 115 | { 116 | // Expand the rectangle to include the specified point. 117 | if (x < rectPos.Left) 118 | { 119 | rectPos.Left = x; 120 | } 121 | 122 | if (x > rectPos.Right) 123 | { 124 | rectPos.Right = x; 125 | } 126 | 127 | if (y < rectPos.Top) 128 | { 129 | rectPos.Top = y; 130 | } 131 | 132 | if (y > rectPos.Bottom) 133 | { 134 | rectPos.Bottom = y; 135 | } 136 | 137 | fPixelFound = true; 138 | } 139 | pixel++; 140 | } 141 | 142 | return fPixelFound; 143 | } 144 | 145 | /// 146 | /// Checks that the block type matches the expected . 147 | /// 148 | /// The block identifier. 149 | /// The expected . 150 | internal static void CheckBlockType(ushort blockID, PSPBlockID expected) 151 | { 152 | if (blockID != (ushort)expected) 153 | { 154 | throw new FormatException(string.Format(Properties.Resources.UnexpectedBlockTypeFormat, 155 | blockID.ToString(CultureInfo.InvariantCulture), 156 | ((ushort)expected).ToString(CultureInfo.InvariantCulture), 157 | expected.ToString())); 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/PSPSections/RGBQUAD.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System.Runtime.InteropServices; 13 | 14 | namespace PaintShopProFiletype.PSPSections 15 | { 16 | [StructLayout(LayoutKind.Sequential)] 17 | internal readonly struct RGBQUAD 18 | { 19 | public readonly byte red; 20 | public readonly byte green; 21 | public readonly byte blue; 22 | public readonly byte reserved; 23 | 24 | public RGBQUAD(IO.EndianBinaryReader reader) 25 | { 26 | this.red = reader.ReadByte(); 27 | this.green = reader.ReadByte(); 28 | this.blue = reader.ReadByte(); 29 | this.reserved = reader.ReadByte(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/PSPSections/RLE.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System; 13 | 14 | namespace PaintShopProFiletype.PSPSections 15 | { 16 | internal static class RLE 17 | { 18 | public static void Decompress(ReadOnlySpan compressedData, Span uncompressedData) 19 | { 20 | int srcIndx = 0; 21 | int dstIndx = 0; 22 | 23 | int len; 24 | byte value; 25 | int compressedLength = compressedData.Length; 26 | int uncompressedLength = uncompressedData.Length; 27 | 28 | while (srcIndx < compressedLength && dstIndx < uncompressedLength) 29 | { 30 | len = compressedData[srcIndx++]; 31 | 32 | if (len > 128) 33 | { 34 | len -= 128; 35 | value = compressedData[srcIndx++]; 36 | 37 | uncompressedData.Slice(dstIndx, len).Fill(value); 38 | } 39 | else 40 | { 41 | compressedData.Slice(srcIndx, len).CopyTo(uncompressedData.Slice(dstIndx, len)); 42 | } 43 | 44 | dstIndx += len; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PSPSections/ThumbnailBlock.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using PaintShopProFiletype.IO; 13 | 14 | namespace PaintShopProFiletype.PSPSections 15 | { 16 | /// 17 | /// Only present in PSP 5 (or earlier?) files 18 | /// 19 | internal sealed class ThumbnailBlock 20 | { 21 | internal int width; 22 | internal int height; 23 | internal ushort bitDepth; 24 | internal PSPCompression compressionType; // ushort 25 | internal ushort planeCount; 26 | internal uint colorCount; 27 | internal uint paletteEntryCount; 28 | internal ColorPaletteBlock? paletteSubBlock; 29 | internal ushort channelCount; 30 | internal ChannelSubBlock[] channelBlocks; 31 | 32 | private const uint InitalBlockLength = 24U; 33 | 34 | public ThumbnailBlock(int width, int height, ChannelSubBlock[] channels) 35 | { 36 | this.width = width; 37 | this.height = height; 38 | this.bitDepth = 24; 39 | this.compressionType = PSPCompression.LZ77; 40 | this.planeCount = 1; 41 | this.colorCount = (1 << 24); 42 | this.paletteEntryCount = 0; 43 | this.paletteSubBlock = null; 44 | this.channelCount = checked((ushort)channels.Length); 45 | this.channelBlocks = channels; 46 | } 47 | 48 | #if DEBUG 49 | public ThumbnailBlock(EndianBinaryReader br) 50 | { 51 | this.width = br.ReadInt32(); 52 | this.height = br.ReadInt32(); 53 | this.bitDepth = br.ReadUInt16(); 54 | this.compressionType = (PSPCompression)br.ReadUInt16(); 55 | this.planeCount = br.ReadUInt16(); 56 | this.colorCount = br.ReadUInt32(); 57 | this.paletteEntryCount = br.ReadUInt32(); 58 | this.channelCount = br.ReadUInt16(); 59 | 60 | this.channelBlocks = new ChannelSubBlock[this.channelCount]; 61 | 62 | int index = 0; 63 | do 64 | { 65 | uint blockSig = br.ReadUInt32(); 66 | if (blockSig != PSPConstants.blockIdentifier) 67 | { 68 | throw new System.FormatException(Properties.Resources.InvalidBlockSignature); 69 | } 70 | PSPBlockID blockType = (PSPBlockID)br.ReadUInt16(); 71 | #pragma warning disable IDE0059 // Value assigned to symbol is never used 72 | uint chunkLength = br.ReadUInt32(); 73 | #pragma warning restore IDE0059 // Value assigned to symbol is never used 74 | 75 | switch (blockType) 76 | { 77 | case PSPBlockID.ColorPalette: 78 | this.paletteSubBlock = new ColorPaletteBlock(br, PSPConstants.majorVersion5); 79 | break; 80 | case PSPBlockID.Channel: 81 | ChannelSubBlock block = new ChannelSubBlock(br, this.compressionType, PSPConstants.majorVersion5); 82 | this.channelBlocks[index] = block; 83 | index++; 84 | break; 85 | } 86 | } 87 | while (index < this.channelCount); 88 | } 89 | #endif 90 | 91 | public void Save(BinaryWriterEx bw) 92 | { 93 | bw.Write(PSPConstants.blockIdentifier); 94 | bw.Write(PSPConstants.v5ThumbnailBlock); 95 | bw.Write(InitalBlockLength); 96 | 97 | using (new BlockLengthWriter(bw)) 98 | { 99 | bw.Write(this.width); 100 | bw.Write(this.height); 101 | bw.Write(this.bitDepth); 102 | bw.Write((ushort)this.compressionType); 103 | bw.Write(this.planeCount); 104 | bw.Write(this.colorCount); 105 | bw.Write(this.paletteEntryCount); 106 | bw.Write(this.channelCount); 107 | 108 | for (int i = 0; i < this.channelCount; i++) 109 | { 110 | this.channelBlocks![i].Save(bw, PSPConstants.majorVersion5); 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/PaintShopProFiletype.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0-windows 4 | Library 5 | false 6 | true 7 | true 8 | true 9 | enable 10 | 11 | 12 | true 13 | AllRules.ruleset 14 | 15 | 16 | none 17 | true 18 | AllRules.ruleset 19 | 20 | 21 | 22 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Base.dll 23 | 24 | 25 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.ComponentModel.dll 26 | 27 | 28 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Core.dll 29 | 30 | 31 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Data.dll 32 | 33 | 34 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Fundamentals.dll 35 | 36 | 37 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Primitives.dll 38 | 39 | 40 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.PropertySystem.dll 41 | 42 | 43 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Windows.dll 44 | 45 | 46 | ..\..\..\..\..\..\Program Files\paint.net\PaintDotNet.Windows.Core.dll 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | True 55 | True 56 | Resources.resx 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/PaintShopProFiletype.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.16 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaintShopProFiletype", "PaintShopProFiletype.csproj", "{C54DB656-EB36-4BC0-A5E5-DAB04982341D}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C54DB656-EB36-4BC0-A5E5-DAB04982341D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C54DB656-EB36-4BC0-A5E5-DAB04982341D}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C54DB656-EB36-4BC0-A5E5-DAB04982341D}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C54DB656-EB36-4BC0-A5E5-DAB04982341D}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /src/PluginSupportInfo.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System; 13 | using System.Reflection; 14 | using PaintDotNet; 15 | 16 | namespace PaintShopProFiletype 17 | { 18 | public sealed class PluginSupportInfo : IPluginSupportInfo 19 | { 20 | public string Author 21 | { 22 | get 23 | { 24 | return "null54"; 25 | } 26 | } 27 | 28 | public string Copyright 29 | { 30 | get 31 | { 32 | return ((AssemblyCopyrightAttribute)typeof(PaintShopProFormat).Assembly.GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false)[0]).Copyright; 33 | } 34 | } 35 | 36 | public string DisplayName 37 | { 38 | get 39 | { 40 | return "Paint Shop Pro Filetype"; 41 | } 42 | } 43 | 44 | public Version Version 45 | { 46 | get 47 | { 48 | return typeof(PaintShopProFormat).Assembly.GetName().Version!; 49 | } 50 | } 51 | 52 | public Uri WebsiteUri 53 | { 54 | get 55 | { 56 | return new Uri("https://forums.getpaint.net/index.php?showtopic=22817"); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////// 2 | // 3 | // This file is part of pdn-pspformat, a FileType plugin for Paint.NET 4 | // 5 | // Copyright (c) 2011-2015, 2019, 2023 Nicholas Hayes 6 | // 7 | // This file is licensed under the MIT License. 8 | // See LICENSE.txt for complete licensing and attribution information. 9 | // 10 | //////////////////////////////////////////////////////////////////////// 11 | 12 | using System.Reflection; 13 | using System.Runtime.InteropServices; 14 | using System.Runtime.Versioning; 15 | 16 | // General Information about an assembly is controlled through the following 17 | // set of attributes. Change these attribute values to modify the information 18 | // associated with an assembly. 19 | [assembly: AssemblyTitle("Paint Shop Pro® Filetype plugin for Paint.NET")] 20 | [assembly: AssemblyDescription("")] 21 | [assembly: AssemblyConfiguration("")] 22 | [assembly: AssemblyCompany("null54")] 23 | [assembly: AssemblyProduct("PaintShopProFiletype")] 24 | [assembly: AssemblyCopyright("Copyright © 2023 Nicholas Hayes (aka null54)")] 25 | [assembly: AssemblyTrademark("")] 26 | [assembly: AssemblyCulture("")] 27 | 28 | // Setting ComVisible to false makes the types in this assembly not visible 29 | // to COM components. If you need to access a type in this assembly from 30 | // COM, set the ComVisible attribute to true on that type. 31 | [assembly: ComVisible(false)] 32 | 33 | // The following GUID is for the ID of the typelib if this project is exposed to COM 34 | [assembly: Guid("bfd6dfcb-8e94-4f95-b840-91f636399fa5")] 35 | 36 | [assembly: SupportedOSPlatform("windows")] 37 | 38 | // Version information for an assembly consists of the following four values: 39 | // 40 | // Major Version 41 | // Minor Version 42 | // Build Number 43 | // Revision 44 | // 45 | // You can specify all the values or you can default the Build and Revision Numbers 46 | // by using the '*' as shown below: 47 | // [assembly: AssemblyVersion("1.0.*")] 48 | [assembly: AssemblyVersion("1.0.3.0")] 49 | [assembly: AssemblyFileVersion("1.0.3.0")] 50 | -------------------------------------------------------------------------------- /src/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.34209 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace PaintShopProFiletype.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PaintShopProFiletype.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The file does not contain a color palette.. 65 | /// 66 | internal static string ColorPaletteNotFound { 67 | get { 68 | return ResourceManager.GetString("ColorPaletteNotFound", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to LZ77. 74 | /// 75 | internal static string CompressionFormatLZ77Text { 76 | get { 77 | return ResourceManager.GetString("CompressionFormatLZ77Text", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to None. 83 | /// 84 | internal static string CompressionFormatNoneText { 85 | get { 86 | return ResourceManager.GetString("CompressionFormatNoneText", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to RLE. 92 | /// 93 | internal static string CompressionFormatRLEText { 94 | get { 95 | return ResourceManager.GetString("CompressionFormatRLEText", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Compression Format. 101 | /// 102 | internal static string CompressionFormatText { 103 | get { 104 | return ResourceManager.GetString("CompressionFormatText", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to File Version. 110 | /// 111 | internal static string FileVersionText { 112 | get { 113 | return ResourceManager.GetString("FileVersionText", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to The file contains an invalid block signature.. 119 | /// 120 | internal static string InvalidBlockSignature { 121 | get { 122 | return ResourceManager.GetString("InvalidBlockSignature", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to The file width and height must be greater than zero.. 128 | /// 129 | internal static string InvalidDocumentDimensions { 130 | get { 131 | return ResourceManager.GetString("InvalidDocumentDimensions", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to The file is not a valid Paint Shop Pro image.. 137 | /// 138 | internal static string InvalidPSPFile { 139 | get { 140 | return ResourceManager.GetString("InvalidPSPFile", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to The selected Paint Shop Pro format supports a maximum of {0} layers.. 146 | /// 147 | internal static string MaxLayersFormat { 148 | get { 149 | return ResourceManager.GetString("MaxLayersFormat", resourceCulture); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized string similar to The file does not contain any Raster layers.. 155 | /// 156 | internal static string RasterLayerNotFound { 157 | get { 158 | return ResourceManager.GetString("RasterLayerNotFound", resourceCulture); 159 | } 160 | } 161 | 162 | /// 163 | /// Looks up a localized string similar to {0} is not an expected block type, expecting {1} ({2}). . 164 | /// 165 | internal static string UnexpectedBlockTypeFormat { 166 | get { 167 | return ResourceManager.GetString("UnexpectedBlockTypeFormat", resourceCulture); 168 | } 169 | } 170 | 171 | /// 172 | /// Looks up a localized string similar to {0}-bit is not a supported image depth.. 173 | /// 174 | internal static string UnsupportedBitDepth { 175 | get { 176 | return ResourceManager.GetString("UnsupportedBitDepth", resourceCulture); 177 | } 178 | } 179 | 180 | /// 181 | /// Looks up a localized string similar to The file uses a version of the Paint Shop Pro format that is not supported by this plugin.. 182 | /// 183 | internal static string UnsupportedFormatVersion { 184 | get { 185 | return ResourceManager.GetString("UnsupportedFormatVersion", resourceCulture); 186 | } 187 | } 188 | 189 | /// 190 | /// Looks up a localized string similar to Paint Shop Pro 5. 191 | /// 192 | internal static string Version5 { 193 | get { 194 | return ResourceManager.GetString("Version5", resourceCulture); 195 | } 196 | } 197 | 198 | /// 199 | /// Looks up a localized string similar to Paint Shop Pro 6, 7, 8, 9. 200 | /// 201 | internal static string Version6AndLater { 202 | get { 203 | return ResourceManager.GetString("Version6AndLater", resourceCulture); 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The file does not contain a color palette. 122 | 123 | 124 | LZ77 125 | 126 | 127 | None 128 | 129 | 130 | RLE 131 | 132 | 133 | Compression Format 134 | 135 | 136 | File Version 137 | 138 | 139 | The file contains an invalid block signature. 140 | 141 | 142 | The file width and height must be greater than zero. 143 | 144 | 145 | The file is not a valid Paint Shop Pro image. 146 | 147 | 148 | The selected Paint Shop Pro format supports a maximum of {0} layers. 149 | 150 | 151 | The file does not contain any Raster layers. 152 | 153 | 154 | {0} is not an expected block type, expecting {1} ({2}). 155 | 156 | 157 | {0}-bit is not a supported image depth. 158 | 159 | 160 | The file uses a version of the Paint Shop Pro format that is not supported by this plugin. 161 | 162 | 163 | Paint Shop Pro 5 164 | 165 | 166 | Paint Shop Pro 6, 7, 8, 9 167 | 168 | --------------------------------------------------------------------------------