├── .gitattributes ├── .github ├── CONTRIBUTING.md └── ISSUE_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── ImageSprites.sln ├── LICENSE ├── README.md ├── appveyor.yml ├── art ├── context-menu-images.png ├── context-menu-update.png ├── context-menu-updateall.png └── sol-exp.png ├── default.ruleset ├── src ├── ImageSprites │ ├── Enums │ │ ├── ImageType.cs │ │ ├── Optimizations.cs │ │ ├── Orientation.cs │ │ └── Stylesheet.cs │ ├── Helpers │ │ └── SpriteHelpers.cs │ ├── ImageSprites.csproj │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── SpriteDocument.cs │ ├── SpriteFragment.cs │ ├── SpriteImageGeneratedEventArgs.cs │ ├── SpriteImageGenerator.cs │ ├── SpriteParseException.cs │ ├── SpriteStylesheetGenerator.cs │ └── packages.config └── ImageSpritesVsix │ ├── Adornments │ ├── AdornmentLayer.cs │ ├── AdornmentProvider.cs │ └── LogoAdornment.cs │ ├── Commands │ ├── CreateSpriteCommand.cs │ ├── SpriteCreationListener.cs │ ├── UpdateAllSpritesCommand.cs │ └── UpdateSpriteCommand.cs │ ├── Constants.cs │ ├── ContentType │ ├── SpriteContentTypeDefinition.cs │ └── registry.pkgdef │ ├── DragDrop │ ├── IgnoreDropHandler.cs │ └── IgnoreDropHandlerProvider.cs │ ├── Extensions.vsext │ ├── Helpers │ ├── Logger.cs │ └── ProjectHelpers.cs │ ├── ImageSpriteCommandTable.cs │ ├── ImageSpriteCommandTable.vsct │ ├── ImageSpritePackage.cs │ ├── ImageSpritesVsix.csproj │ ├── Properties │ └── AssemblyInfo.cs │ ├── Resources │ ├── Icon.png │ └── Preview.png │ ├── SpriteService.cs │ ├── source.extension.cs │ └── source.extension.vsixmanifest └── test └── ImageSpritesTest ├── Artifacts ├── Images │ ├── 384dpi │ │ ├── a.png │ │ ├── b.png │ │ ├── c.png │ │ ├── d.png │ │ ├── e.png │ │ └── f.png │ └── 96dpi │ │ ├── a.png │ │ ├── b.png │ │ ├── c.png │ │ ├── d.png │ │ ├── e.png │ │ └── f.png ├── hash.sprite ├── png384.sprite └── png96.sprite ├── GenerateTest.cs ├── ImageSpritesTest.csproj ├── Properties └── AssemblyInfo.cs ├── SerializationTest.cs └── packages.config /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Looking to contribute something? **Here's how you can help.** 4 | 5 | Please take a moment to review this document in order to make the contribution 6 | process easy and effective for everyone involved. 7 | 8 | Following these guidelines helps to communicate that you respect the time of 9 | the developers managing and developing this open source project. In return, 10 | they should reciprocate that respect in addressing your issue or assessing 11 | patches and features. 12 | 13 | 14 | ## Using the issue tracker 15 | 16 | The issue tracker is the preferred channel for [bug reports](#bug-reports), 17 | [features requests](#feature-requests) and 18 | [submitting pull requests](#pull-requests), but please respect the 19 | following restrictions: 20 | 21 | * Please **do not** use the issue tracker for personal support requests. Stack 22 | Overflow is a better place to get help. 23 | 24 | * Please **do not** derail or troll issues. Keep the discussion on topic and 25 | respect the opinions of others. 26 | 27 | * Please **do not** open issues or pull requests which *belongs to* third party 28 | components. 29 | 30 | 31 | ## Bug reports 32 | 33 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 34 | Good bug reports are extremely helpful, so thanks! 35 | 36 | Guidelines for bug reports: 37 | 38 | 1. **Use the GitHub issue search** — check if the issue has already been 39 | reported. 40 | 41 | 2. **Check if the issue has been fixed** — try to reproduce it using the 42 | latest `master` or development branch in the repository. 43 | 44 | 3. **Isolate the problem** — ideally create an 45 | [SSCCE](http://www.sscce.org/) and a live example. 46 | Uploading the project on cloud storage (OneDrive, DropBox, et el.) 47 | or creating a sample GitHub repository is also helpful. 48 | 49 | 50 | A good bug report shouldn't leave others needing to chase you up for more 51 | information. Please try to be as detailed as possible in your report. What is 52 | your environment? What steps will reproduce the issue? What browser(s) and OS 53 | experience the problem? Do other browsers show the bug differently? What 54 | would you expect to be the outcome? All these details will help people to fix 55 | any potential bugs. 56 | 57 | Example: 58 | 59 | > Short and descriptive example bug report title 60 | > 61 | > A summary of the issue and the Visual Studio, browser, OS environments 62 | > in which it occurs. If suitable, include the steps required to reproduce the bug. 63 | > 64 | > 1. This is the first step 65 | > 2. This is the second step 66 | > 3. Further steps, etc. 67 | > 68 | > `` - a link to the project/file uploaded on cloud storage or other publicly accessible medium. 69 | > 70 | > Any other information you want to share that is relevant to the issue being 71 | > reported. This might include the lines of code that you have identified as 72 | > causing the bug, and potential solutions (and your opinions on their 73 | > merits). 74 | 75 | 76 | ## Feature requests 77 | 78 | Feature requests are welcome. But take a moment to find out whether your idea 79 | fits with the scope and aims of the project. It's up to *you* to make a strong 80 | case to convince the project's developers of the merits of this feature. Please 81 | provide as much detail and context as possible. 82 | 83 | 84 | ## Pull requests 85 | 86 | Good pull requests, patches, improvements and new features are a fantastic 87 | help. They should remain focused in scope and avoid containing unrelated 88 | commits. 89 | 90 | **Please ask first** before embarking on any significant pull request (e.g. 91 | implementing features, refactoring code, porting to a different language), 92 | otherwise you risk spending a lot of time working on something that the 93 | project's developers might not want to merge into the project. 94 | 95 | Please adhere to the [coding guidelines](#code-guidelines) used throughout the 96 | project (indentation, accurate comments, etc.) and any other requirements 97 | (such as test coverage). 98 | 99 | Adhering to the following process is the best way to get your work 100 | included in the project: 101 | 102 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 103 | and configure the remotes: 104 | 105 | ```bash 106 | # Clone your fork of the repo into the current directory 107 | git clone https://github.com//.git 108 | # Navigate to the newly cloned directory 109 | cd 110 | # Assign the original repo to a remote called "upstream" 111 | git remote add upstream https://github.com/madskristensen/.git 112 | ``` 113 | 114 | 2. If you cloned a while ago, get the latest changes from upstream: 115 | 116 | ```bash 117 | git checkout master 118 | git pull upstream master 119 | ``` 120 | 121 | 3. Create a new topic branch (off the main project development branch) to 122 | contain your feature, change, or fix: 123 | 124 | ```bash 125 | git checkout -b 126 | ``` 127 | 128 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 129 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 130 | or your code is unlikely be merged into the main project. Use Git's 131 | [interactive rebase](https://help.github.com/articles/interactive-rebase) 132 | feature to tidy up your commits before making them public. Also, prepend name of the feature 133 | to the commit message. For instance: "SCSS: Fixes compiler results for IFileListener.\nFixes `#123`" 134 | 135 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 136 | 137 | ```bash 138 | git pull [--rebase] upstream master 139 | ``` 140 | 141 | 6. Push your topic branch up to your fork: 142 | 143 | ```bash 144 | git push origin 145 | ``` 146 | 147 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 148 | with a clear title and description against the `master` branch. 149 | 150 | 151 | ## Code guidelines 152 | 153 | - Always use proper indentation. 154 | - In Visual Studio under `Tools > Options > Text Editor > C# > Advanced`, make sure 155 | `Place 'System' directives first when sorting usings` option is enabled (checked). 156 | - Before committing, organize usings for each updated C# source file. Either you can 157 | right-click editor and select `Organize Usings > Remove and sort` OR use extension 158 | like [BatchFormat](http://visualstudiogallery.msdn.microsoft.com/a7f75c34-82b4-4357-9c66-c18e32b9393e). 159 | - Before committing, run Code Analysis in `Debug` configuration and follow the guidelines 160 | to fix CA issues. Code Analysis commits can be made separately. 161 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Installed product versions 2 | - Visual Studio: [example 2015 Professional] 3 | - This extension: [example 1.1.21] 4 | 5 | ### Description 6 | Replace this text with a short description 7 | 8 | ### Steps to recreate 9 | 1. Replace this 10 | 2. text with 11 | 3. the steps 12 | 4. to recreate 13 | 14 | ### Current behavior 15 | Explain what it's doing and why it's wrong 16 | 17 | ### Expected behavior 18 | Explain what it should be doing after it's fixed. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | 3 | # User files 4 | *.suo 5 | *.user 6 | *.sln.docstates 7 | .vs/ 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Rr]elease/ 12 | x64/ 13 | [Bb]in/ 14 | [Oo]bj/ 15 | 16 | # MSTest test Results 17 | [Tt]est[Rr]esult*/ 18 | [Bb]uild[Ll]og.* 19 | 20 | # NCrunch 21 | *.ncrunchsolution 22 | *.ncrunchproject 23 | _NCrunch_WebCompiler -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Road map 2 | 3 | - [ ] Nothing yet... 4 | 5 | Features that have a checkmark are complete and available for 6 | download in the 7 | [CI build](http://vsixgallery.com/extension/cd92c0c6-2c32-49a3-83ca-0dc767c7d78e/). 8 | 9 | # Change log 10 | 11 | These are the changes to each version that has been released 12 | on the official Visual Studio extension gallery. 13 | 14 | ## 1.4 15 | 16 | - [x] Stylus support 17 | 18 | ## 1.3 19 | 20 | - [x] Extension load optimizations 21 | - [x] Memory allocation fixes 22 | 23 | ## 1.2 24 | 25 | - [x] Uploading JSON schema to [SchemaStore.org](http://schemastore.org) 26 | - [x] Reload schemas when .sprite file is opened 27 | - [x] Support for "VS15" 28 | - [x] Use the version of JSON.NET shipped in VS 29 | - [x] Add custom CSS declarations to the generated stylesheet 30 | 31 | ## 1.1 32 | 33 | - [x] Updated format of .sprite JSON to use name-file pairs 34 | - [x] Generated file names all contain *.sprite* in them 35 | - [x] Support drag 'n drop of image files onto .sprite file 36 | - [x] Open the .sprite file after it's created 37 | - [x] Command to update all sprites in solution 38 | - [x] Updated format to be more self-explanatory 39 | - [x] Logo watermark in .sprite files 40 | 41 | ## 1.0 42 | 43 | - [x] Sprite image generation 44 | - [x] Command for creating .sprite files 45 | - [x] Command for updating sprites 46 | - [x] Generate LESS/Sass/Css file 47 | - [x] Add JSON schema 48 | - [x] Image Optimizer integration 49 | - [x] File nesting 50 | - [x] .sprite parse error handling 51 | - [x] Input file not found error handling 52 | - [x] Screenshots and updated readme.md 53 | - [x] Correct defaults based on input images 54 | - [x] Configurable DPI settings 55 | - [x] Document how to use the LESS/Sass/Css files -------------------------------------------------------------------------------- /ImageSprites.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25123.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSpritesVsix", "src\ImageSpritesVsix\ImageSpritesVsix.csproj", "{D6AC6895-FAB6-4B0A-96AE-4FC1EFBB758E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSprites", "src\ImageSprites\ImageSprites.csproj", "{AB5AE1E7-8362-4A97-A8F9-3939BE8470C4}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSpritesTest", "test\ImageSpritesTest\ImageSpritesTest.csproj", "{6A23F2E8-DEFF-48AB-8844-D55D811229C7}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B7B668AA-ADC5-4D1A-A42B-B7B02C99C6C7}" 13 | ProjectSection(SolutionItems) = preProject 14 | appveyor.yml = appveyor.yml 15 | CHANGELOG.md = CHANGELOG.md 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {D6AC6895-FAB6-4B0A-96AE-4FC1EFBB758E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {D6AC6895-FAB6-4B0A-96AE-4FC1EFBB758E}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {D6AC6895-FAB6-4B0A-96AE-4FC1EFBB758E}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {D6AC6895-FAB6-4B0A-96AE-4FC1EFBB758E}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {AB5AE1E7-8362-4A97-A8F9-3939BE8470C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {AB5AE1E7-8362-4A97-A8F9-3939BE8470C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {AB5AE1E7-8362-4A97-A8F9-3939BE8470C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {AB5AE1E7-8362-4A97-A8F9-3939BE8470C4}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {6A23F2E8-DEFF-48AB-8844-D55D811229C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {6A23F2E8-DEFF-48AB-8844-D55D811229C7}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {6A23F2E8-DEFF-48AB-8844-D55D811229C7}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {6A23F2E8-DEFF-48AB-8844-D55D811229C7}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Mads Kristensen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Sprites 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/ox04djmajibm3qqv?svg=true)](https://ci.appveyor.com/project/madskristensen/imagesprites) 4 | 5 | Download this extension from the [VS Gallery](https://visualstudiogallery.msdn.microsoft.com/8bb845e9-5717-4eae-aed3-1fdf6fe5819a) 6 | or get the [CI build](http://vsixgallery.com/extension/cd92c0c6-2c32-49a3-83ca-0dc767c7d78e/). 7 | 8 | --------------------------------------- 9 | 10 | An image sprite is a collection of images put into a single 11 | image. 12 | 13 | A web page with many images can take a long time to load 14 | and generates multiple server requests. 15 | 16 | Using image sprites will reduce the number of server 17 | requests and save bandwidth. 18 | 19 | This extension makes it easier than ever to create, maintain 20 | and use image sprites in any web project. 21 | 22 | See the [changelog](CHANGELOG.md) for changes and roadmap. 23 | 24 | ## Features 25 | 26 | - Easy to create and update image sprites 27 | - Supports png, jpg and gif images 28 | - Configure vertical or horizontal sprite layouts 29 | - Produce LESS, Sass, Stylus, or CSS file with sprite image locations 30 | - [Image Optimizer](https://visualstudiogallery.msdn.microsoft.com/a56eddd3-d79b-48ac-8c8f-2db06ade77c3) integration 31 | - Configurable DPI for high resolution images 32 | - Works with both [Web Compiler](https://visualstudiogallery.msdn.microsoft.com/3b329021-cd7a-4a01-86fc-714c2d05bb6c) 33 | and [Bundler & Minifier](https://visualstudiogallery.msdn.microsoft.com/9ec27da7-e24b-4d56-8064-fd7e88ac1c40) 34 | - Drag 'n drop of image files supported 35 | 36 | ### Create image sprite 37 | Select the images in Solution Explorer and click 38 | *Create image Sprite* from the context menu. 39 | 40 | ![Context menu](art/context-menu-images.png) 41 | 42 | This will generate a **.sprite** manifest file as well as 43 | the resulting image file and a **.css** file. 44 | 45 | ![Sol Exp](art/sol-exp.png) 46 | 47 | ### The .sprite file 48 | The .sprite file is where information about the image sprite 49 | is stored. It looks something like this: 50 | 51 | ```json 52 | { 53 | "images": { 54 | "pic1": "a.png", 55 | "pic2": "b.png" 56 | }, 57 | "orientation": "vertical", 58 | "optimize": "lossless", 59 | "padding": 10, 60 | "output": "png", 61 | "dpi": 384, 62 | "stylesheet": "css", 63 | "pathprefix": "/images/", 64 | "customstyles": { 65 | "display": "inline-block" 66 | } 67 | } 68 | ``` 69 | 70 | **images** is an array of relative file paths to the image 71 | files that make up the resulting sprite file. The order 72 | of the images are maintained in the generated sprite image. 73 | The name of the image will be persisted in the generated 74 | stylesheet as class names. 75 | 76 | **orientation** determines if the images are layed out either 77 | horizontally or vertically. 78 | 79 | **padding** is the distance of whitespace inserted around each 80 | individual image in the sprite. The value is in pixels. 81 | 82 | **dpi** sets the resolution of the image. 96 is the default value. 83 | 84 | **optimize** controls how the generated image sprite should be 85 | optimized. Choose from *lossless*, *lossy* or *none*. This 86 | feature requires the 87 | [Images Optimizer](https://visualstudiogallery.msdn.microsoft.com/a56eddd3-d79b-48ac-8c8f-2db06ade77c3) 88 | to be installed. 89 | 90 | **stylesheet** outputs LESS, Sass, Stylus, or plain CSS files to make 91 | it easy to use the image sprite in any web project. 92 | 93 | **pathprefix** adds a prefix string to the image path in 94 | the *url(path)* value in the stylesheet. 95 | 96 | **customstyles** allows you to inject any css declarations 97 | into the generated stylesheets. 98 | 99 | ### Update image sprite 100 | Every time the .sprite file is modified and saved, the image 101 | sprite and optional stylesheets are updated. 102 | 103 | A button is also located on the context-menu of .sprite files 104 | to make it even easier. 105 | 106 | ![Context menu update](art/context-menu-update.png) 107 | 108 | You can update all image sprites by right-clicking on either 109 | the project or solution and select *Update All Image Sprites*. 110 | 111 | ![Context menu update all](art/context-menu-updateall.png) 112 | 113 | ## Consume the sprite 114 | You can use the sprite from CSS, LESS or Sass. 115 | 116 | ### CSS 117 | Make sure to configure the .sprite to produce a .css file. 118 | Here's how to do that: 119 | 120 | ```json 121 | "stylesheets": "css" 122 | ``` 123 | 124 | That will produce a file called something like *mysprite.sprite.css* 125 | nested under the *mysprite.sprite* file. 126 | 127 | All you have to do is to include the .css file in your HTML 128 | like so: 129 | 130 | ```html 131 | 132 | ``` 133 | 134 | You can then add HTML markup with 2 class names. The first 135 | class name is the name of the .sprite file. In this case 136 | *mysprite*. The other class name is the name of the individual 137 | image in the sprite you wish to inject. 138 | 139 | ```html 140 |
141 | ``` 142 | 143 | ### LESS and Sass 144 | Make sure to configure the .sprite to produce a .less or a 145 | .scss file. Here's how to do that: 146 | 147 | ```json 148 | "stylesheets": "less|sass" 149 | ``` 150 | 151 | Then import the generated .less file into the .less files that 152 | will consume the mixins generated by this extension. 153 | 154 | **LESS** 155 | ```less 156 | @import "mysprite.sprite.less"; 157 | 158 | .myclass { 159 | .mysprite-pic1(); 160 | } 161 | ``` 162 | 163 | **Sass** 164 | ```scss 165 | @import "mysprite.sprite.scss"; 166 | 167 | .myclass { 168 | @include mysprite-pic1(); 169 | } 170 | ``` 171 | 172 | That will produce the following CSS: 173 | 174 | ```css 175 | .myclass { 176 | width: 16px; 177 | height: 16px; 178 | display: block; 179 | background: url('mysprite.sprite.png') -36px -10px no-repeat; 180 | } 181 | ``` 182 | 183 | To use the generated CSS on your page, see the above section 184 | on *CSS*. 185 | 186 | ## Contribute 187 | Check out the [contribution guidelines](.github/CONTRIBUTING.md) 188 | if you want to contribute to this project. 189 | 190 | For cloning and building this project yourself, make sure 191 | to install the 192 | [Extensibility Tools 2015](https://visualstudiogallery.msdn.microsoft.com/ab39a092-1343-46e2-b0f1-6a3f91155aa6) 193 | extension for Visual Studio which enables some features 194 | used by this project. 195 | 196 | ## License 197 | [Apache 2.0](LICENSE) -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2019 2 | 3 | install: 4 | - ps: (new-object Net.WebClient).DownloadString("https://raw.github.com/madskristensen/ExtensionScripts/master/AppVeyor/vsix.ps1") | iex 5 | 6 | before_build: 7 | - ps: Vsix-IncrementVsixVersion | Vsix-UpdateBuildVersion 8 | - ps: Vsix-TokenReplacement src\ImageSpritesVsix\source.extension.cs 'Version = "([0-9\\.]+)"' 'Version = "{version}"' 9 | 10 | build_script: 11 | - nuget restore -Verbosity quiet 12 | - msbuild /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m 13 | 14 | after_test: 15 | - ps: Vsix-PushArtifacts | Vsix-PublishToGallery -------------------------------------------------------------------------------- /art/context-menu-images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/art/context-menu-images.png -------------------------------------------------------------------------------- /art/context-menu-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/art/context-menu-update.png -------------------------------------------------------------------------------- /art/context-menu-updateall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/art/context-menu-updateall.png -------------------------------------------------------------------------------- /art/sol-exp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/art/sol-exp.png -------------------------------------------------------------------------------- /default.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ImageSprites/Enums/ImageType.cs: -------------------------------------------------------------------------------- 1 | namespace ImageSprites 2 | { 3 | /// 4 | /// Image format type. 5 | /// 6 | public enum ImageType 7 | { 8 | /// Image sprite will produce a .png file. 9 | Png, 10 | 11 | /// Image sprite will produce a .jpg file. 12 | Jpg, 13 | 14 | /// Image sprite will produce a .gif file. 15 | Gif 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ImageSprites/Enums/Optimizations.cs: -------------------------------------------------------------------------------- 1 | namespace ImageSprites 2 | { 3 | /// 4 | /// Image optimization types used to compress and optimmize the output sprite image. 5 | /// 6 | public enum Optimization 7 | { 8 | /// No optimization will by applied. 9 | None, 10 | 11 | /// No quality loss but the image might benefit from more compression. 12 | Lossless, 13 | 14 | /// Maximum optimization with some but limited quality loss. 15 | Lossy 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ImageSprites/Enums/Orientation.cs: -------------------------------------------------------------------------------- 1 | namespace ImageSprites 2 | { 3 | /// 4 | /// Orientation of the individual files of an image sprite 5 | /// 6 | public enum Orientation 7 | { 8 | /// The files are layed out vertically. 9 | Vertical, 10 | 11 | /// The files are layed out horizontally. 12 | Horizontal 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ImageSprites/Enums/Stylesheet.cs: -------------------------------------------------------------------------------- 1 | namespace ImageSprites 2 | { 3 | /// 4 | /// The type of stylesheets that the sprite exporter can produce. 5 | /// 6 | public enum Stylesheet 7 | { 8 | /// No stylesheet should be produced. 9 | None, 10 | 11 | /// Produce a .css file. 12 | Css, 13 | 14 | /// Produce a .less file. 15 | Less, 16 | 17 | /// Produce a .scss file. 18 | Scss, 19 | 20 | /// Produce a .styl file. 21 | Styl 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ImageSprites/Helpers/SpriteHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing.Imaging; 3 | using System.IO; 4 | 5 | namespace ImageSprites 6 | { 7 | /// 8 | /// A collection of methods that are helpful for working with image sprites. 9 | /// 10 | public static class SpriteHelpers 11 | { 12 | /// 13 | /// Calculates the relative path between two files. 14 | /// 15 | public static string MakeRelative(string fileBase, string file) 16 | { 17 | Uri one = new Uri(fileBase); 18 | Uri two = new Uri(file); 19 | 20 | return one.MakeRelativeUri(two).ToString().Replace("\\", "/"); 21 | } 22 | 23 | /// 24 | /// Calculates an identifier based on the provided file. 25 | /// 26 | public static string GetIdentifier(string imageFile) 27 | { 28 | return Path.GetFileNameWithoutExtension(imageFile) 29 | .ToLowerInvariant() 30 | .Replace(" ", string.Empty); 31 | } 32 | 33 | internal static ImageFormat ExtensionFromFormat(ImageType format) 34 | { 35 | switch (format) 36 | { 37 | case ImageType.Jpg: 38 | return ImageFormat.Jpeg; 39 | case ImageType.Gif: 40 | return ImageFormat.Gif; 41 | } 42 | 43 | return ImageFormat.Png; 44 | } 45 | 46 | internal static ImageType GetImageFormatFromExtension(string file) 47 | { 48 | string extension = Path.GetExtension(file); 49 | 50 | switch (extension.ToUpperInvariant()) 51 | { 52 | case ".JPG": 53 | case ".JPEG": 54 | return ImageType.Jpg; 55 | 56 | case ".GIF": 57 | return ImageType.Gif; 58 | } 59 | 60 | return ImageType.Png; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/ImageSprites/ImageSprites.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {AB5AE1E7-8362-4A97-A8F9-3939BE8470C4} 8 | Library 9 | Properties 10 | ImageSprites 11 | ImageSprites 12 | v4.5.1 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | bin\Debug\ImageSprites.XML 24 | true 25 | ..\..\default.ruleset 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 38 | True 39 | False 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 71 | -------------------------------------------------------------------------------- /src/ImageSprites/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | 6 | [assembly: AssemblyTitle("Image Sprites")] 7 | [assembly: AssemblyDescription("Produces images sprites from individual images.")] 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("Mads Kristensen")] 10 | [assembly: AssemblyProduct("Image Sprites")] 11 | [assembly: AssemblyCopyright("Copyright © 2016")] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | [assembly: InternalsVisibleTo("ImageSpritesTest")] 16 | 17 | [assembly: CLSCompliant(true)] 18 | [assembly: ComVisible(false)] 19 | [assembly: Guid("ab5ae1e7-8362-4a97-a8f9-3939be8470c4")] 20 | 21 | [assembly: AssemblyVersion("1.0.0.0")] 22 | [assembly: AssemblyFileVersion("1.0.0.0")] 23 | -------------------------------------------------------------------------------- /src/ImageSprites/SpriteDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Converters; 9 | 10 | namespace ImageSprites 11 | { 12 | /// 13 | /// The sprite manifest document containing all information needed to produce the image sprite. 14 | /// 15 | public class SpriteDocument 16 | { 17 | /// Creates a new instance with default values. 18 | public SpriteDocument() 19 | { } 20 | 21 | /// Creates a new instance and calculates values based in provided images. 22 | public SpriteDocument(string fileName, IEnumerable images) 23 | { 24 | FileName = fileName; 25 | AddImages(images); 26 | 27 | if (images.Any()) 28 | { 29 | var first = Image.FromFile(images.First()); 30 | Dpi = (int)Math.Round(first.HorizontalResolution); 31 | Output = SpriteHelpers.GetImageFormatFromExtension(images.First()); 32 | } 33 | } 34 | 35 | /// The absolute file name. 36 | [JsonIgnore] 37 | public string FileName { get; private set; } 38 | 39 | /// The individual images that makes up the sprite. 40 | [JsonProperty("images")] 41 | public IDictionary Images { get; private set; } 42 | 43 | /// The orientation of the individual images inside the sprite. 44 | [JsonProperty("orientation")] 45 | public Orientation Orientation { get; set; } = Orientation.Vertical; 46 | 47 | /// Image optimization settings. 48 | [JsonProperty("optimize")] 49 | public Optimization Optimize { get; set; } = Optimization.Lossless; 50 | 51 | /// The padding size in pixels around each individual image in the sprite. 52 | [JsonProperty("padding")] 53 | public int Padding { get; set; } = 10; 54 | 55 | /// The output format of the generated sprite image. 56 | [JsonProperty("output")] 57 | public ImageType Output { get; set; } = ImageType.Png; 58 | 59 | /// The DPI of the generated sprite image. 60 | [JsonProperty("dpi")] 61 | public int Dpi { get; set; } = 96; 62 | 63 | /// The type of stylesheet to generate. 64 | [JsonProperty("stylesheet")] 65 | public Stylesheet Stylesheet { get; set; } = Stylesheet.None; 66 | 67 | /// The path to prepend to url in the stylesheet's "url()" function. 68 | [JsonProperty("pathprefix")] 69 | public string PathPrefix { get; set; } = string.Empty; 70 | 71 | /// The path to prepend to url in the stylesheet's "url()" function. 72 | [JsonProperty("customstyles")] 73 | public IDictionary CustomStyles { get; } = new Dictionary { { "display", "inline-block" } }; 74 | 75 | /// Flag that determines whether the sprite url in the generated stylesheet will be suffixed by a unique hash for cache busting purposes. 76 | [JsonProperty("appendcachebustspritesuffix")] 77 | public bool AppendCacheBustSpriteSuffix { get; set; } = false; 78 | 79 | /// The file extension of the output sprite image. 80 | [JsonIgnore] 81 | public string OutputExtension 82 | { 83 | get 84 | { 85 | return "." + Output.ToString().ToLowerInvariant(); 86 | } 87 | } 88 | 89 | /// 90 | /// Create an instance from a .sprite file. 91 | /// 92 | /// A valid .sprite JSON file. 93 | public static async Task FromFile(string fileName) 94 | { 95 | using (var reader = new StreamReader(fileName)) 96 | { 97 | var content = await reader.ReadToEndAsync().ConfigureAwait(false); 98 | var doc = FromJSON(content, fileName); 99 | 100 | doc.FileName = fileName; 101 | 102 | return doc; 103 | } 104 | } 105 | 106 | /// 107 | /// Creates an instance from the specified JSON string. 108 | /// 109 | /// A string of valid JSON. 110 | /// Optionally provide a file name for error handling purposes. 111 | /// 112 | public static SpriteDocument FromJSON(string json, string fileName) 113 | { 114 | try 115 | { 116 | return JsonConvert.DeserializeObject(json); 117 | } 118 | catch (JsonSerializationException ex) 119 | { 120 | throw new SpriteParseException(fileName, ex); 121 | } 122 | } 123 | 124 | /// 125 | /// Converts the SpriteDocument to its JSON string representation. 126 | /// 127 | /// A JSON string representation of SpriteDocument instance. 128 | public string ToJsonString() 129 | { 130 | var settings = new JsonSerializerSettings(); 131 | settings.Formatting = Formatting.Indented; 132 | settings.Converters.Add(new StringEnumConverter { CamelCaseText = true }); 133 | 134 | return JsonConvert.SerializeObject(this, settings); 135 | } 136 | 137 | /// 138 | /// Add image files to the sprite. 139 | /// 140 | /// A list of relative file paths. 141 | private void AddImages(IEnumerable files) 142 | { 143 | var dic = new Dictionary(); 144 | 145 | foreach (var file in files) 146 | { 147 | string name = SpriteHelpers.GetIdentifier(file); 148 | string relative = SpriteHelpers.MakeRelative(FileName, file); 149 | 150 | if (dic.ContainsKey(name)) 151 | name += "_" + Guid.NewGuid().ToString().Replace("-", string.Empty); 152 | 153 | dic.Add(name, relative); 154 | } 155 | 156 | Images = dic; 157 | } 158 | 159 | /// 160 | /// Saves the SpriteDocument as a JSON file to the FileName location. 161 | /// 162 | public async Task Save() 163 | { 164 | var json = ToJsonString(); 165 | 166 | using (var writer = new StreamWriter(FileName)) 167 | { 168 | OnSaving(FileName); 169 | await writer.WriteAsync(json).ConfigureAwait(false); 170 | OnSaved(FileName); 171 | } 172 | } 173 | 174 | internal IDictionary ToAbsoluteImages() 175 | { 176 | var dir = Path.GetDirectoryName(FileName); 177 | var dic = new Dictionary(); 178 | 179 | foreach (var ident in Images.Keys) 180 | { 181 | string path = Uri.UnescapeDataString(Images[ident]); 182 | dic.Add(ident, new FileInfo(Path.Combine(dir, path)).FullName); 183 | } 184 | 185 | return dic; 186 | } 187 | 188 | internal void OnSaving(string fileName) 189 | { 190 | if (Saving != null) 191 | { 192 | var type = File.Exists(fileName) ? WatcherChangeTypes.Changed : WatcherChangeTypes.Created; 193 | var dir = Path.GetDirectoryName(fileName); 194 | var name = Path.GetFileName(fileName); 195 | 196 | Saving(this, new FileSystemEventArgs(type, dir, name)); 197 | } 198 | } 199 | 200 | internal void OnSaved(string fileName) 201 | { 202 | if (Saved != null) 203 | { 204 | var type = File.Exists(fileName) ? WatcherChangeTypes.Changed : WatcherChangeTypes.Created; 205 | var dir = Path.GetDirectoryName(fileName); 206 | var name = Path.GetFileName(fileName); 207 | 208 | Saved(this, new FileSystemEventArgs(type, dir, name)); 209 | } 210 | } 211 | 212 | /// Fires before a file is written to disk. 213 | public static event FileSystemEventHandler Saving; 214 | 215 | /// Fires after a file is written to disk. 216 | public static event FileSystemEventHandler Saved; 217 | 218 | /// Internal use only. Used by the JSON.NET serializer 219 | public bool ShouldSerializeDpi() 220 | { 221 | return Dpi != 96; 222 | } 223 | 224 | /// Internal use only. Used by the JSON.NET serializer 225 | public bool ShouldSerializeStylesheet() 226 | { 227 | return Stylesheet != Stylesheet.None; 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/ImageSprites/SpriteFragment.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace ImageSprites 3 | { 4 | internal class SpriteFragment 5 | { 6 | public SpriteFragment(string id, int width, int height, int x, int y) 7 | { 8 | ID = id; 9 | Width = width; 10 | Height = height; 11 | X = x; 12 | Y = y; 13 | } 14 | 15 | public string ID { get; set; } 16 | public int Width { get; set; } 17 | public int Height { get; set; } 18 | public int X { get; set; } 19 | public int Y { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/ImageSprites/SpriteImageGeneratedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImageSprites 4 | { 5 | /// 6 | /// EventArgs for sprite image generation. 7 | /// 8 | public class SpriteImageGenerationEventArgs: EventArgs 9 | { 10 | /// 11 | /// Creates a new instance. 12 | /// 13 | public SpriteImageGenerationEventArgs(string fileName, SpriteDocument document) 14 | { 15 | FileName = fileName; 16 | Document = document; 17 | } 18 | 19 | /// The name of the file being generated. 20 | public string FileName { get; set; } 21 | 22 | /// The used in the generation. 23 | public SpriteDocument Document { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ImageSprites/SpriteImageGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace ImageSprites 9 | { 10 | /// 11 | /// A generator for producing image sprites. 12 | /// 13 | public class SpriteImageGenerator 14 | { 15 | /// 16 | /// Generates an image sprite based on the specified . 17 | /// 18 | public async Task Generate(SpriteDocument doc) 19 | { 20 | var images = GetImages(doc); 21 | 22 | int width = doc.Orientation == Orientation.Vertical ? images.Values.Max(i => i.Width) + (doc.Padding * 2) : images.Values.Sum(i => i.Width) + (doc.Padding * images.Count) + doc.Padding; 23 | int height = doc.Orientation == Orientation.Vertical ? images.Values.Sum(img => img.Height) + (doc.Padding * images.Count) + doc.Padding : images.Values.Max(img => img.Height) + (doc.Padding * 2); 24 | 25 | List fragments = new List(); 26 | 27 | using (var bitmap = new Bitmap(width, height)) 28 | { 29 | bitmap.SetResolution(doc.Dpi, doc.Dpi); 30 | 31 | using (Graphics canvas = Graphics.FromImage(bitmap)) 32 | { 33 | if (doc.Orientation == Orientation.Vertical) 34 | Vertical(images, fragments, canvas, doc.Padding); 35 | else 36 | Horizontal(images, fragments, canvas, doc.Padding); 37 | } 38 | 39 | string outputFile = doc.FileName + doc.OutputExtension; 40 | 41 | OnSaving(outputFile, doc); 42 | bitmap.Save(outputFile, SpriteHelpers.ExtensionFromFormat(doc.Output)); 43 | OnSaved(outputFile, doc); 44 | } 45 | 46 | // Clean up 47 | foreach (var image in images.Values) 48 | { 49 | image.Dispose(); 50 | } 51 | 52 | await SpriteStylesheetGenerator.ExportStylesheet(fragments, doc, this); 53 | } 54 | 55 | private static void Vertical(Dictionary images, List fragments, Graphics canvas, int margin) 56 | { 57 | int currentY = margin; 58 | 59 | foreach (string ident in images.Keys) 60 | { 61 | var img = images[ident]; 62 | fragments.Add(new SpriteFragment(ident, img.Width, img.Height, margin, currentY)); 63 | 64 | canvas.DrawImage(img, margin, currentY); 65 | currentY += img.Height + margin; 66 | } 67 | } 68 | 69 | private static void Horizontal(Dictionary images, List fragments, Graphics canvas, int margin) 70 | { 71 | int currentX = margin; 72 | 73 | foreach (string ident in images.Keys) 74 | { 75 | var img = images[ident]; 76 | fragments.Add(new SpriteFragment(ident, img.Width, img.Height, currentX, margin)); 77 | 78 | canvas.DrawImage(img, currentX, margin); 79 | currentX += img.Width + margin; 80 | } 81 | } 82 | 83 | private static Dictionary GetImages(SpriteDocument doc) 84 | { 85 | var images = new Dictionary(); 86 | var source = doc.ToAbsoluteImages(); 87 | 88 | foreach (string ident in source.Keys) 89 | { 90 | string file = source[ident]; 91 | 92 | if (!File.Exists(file)) 93 | { 94 | throw new FileNotFoundException("One or more sprite input files don't exist", file); 95 | } 96 | 97 | var bitmap = (Bitmap)Image.FromFile(file); 98 | bitmap.SetResolution(doc.Dpi, doc.Dpi); 99 | 100 | images.Add(ident, bitmap); 101 | } 102 | 103 | return images; 104 | } 105 | 106 | internal void OnSaving(string fileName, SpriteDocument doc) 107 | { 108 | Saving?.Invoke(this, new SpriteImageGenerationEventArgs(fileName, doc)); 109 | } 110 | 111 | internal void OnSaved(string fileName, SpriteDocument doc) 112 | { 113 | Saved?.Invoke(this, new SpriteImageGenerationEventArgs(fileName, doc)); 114 | } 115 | 116 | /// Fires before a file is written to disk. 117 | public event EventHandler Saving; 118 | 119 | /// Fires after a file is written to disk. 120 | public event EventHandler Saved; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/ImageSprites/SpriteParseException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace ImageSprites 5 | { 6 | /// 7 | /// An exception type for JSON sprite manifest syntax errors. 8 | /// 9 | [Serializable] 10 | public class SpriteParseException : Exception 11 | { 12 | /// 13 | /// Creates a new instance. 14 | /// 15 | public SpriteParseException() 16 | : base() 17 | { } 18 | 19 | /// 20 | /// Creates an new instance and sets the FileName property. 21 | /// 22 | public SpriteParseException(string FileName) 23 | : this(FileName, null) 24 | { } 25 | 26 | /// 27 | /// Creates an new instance and sets the FileName and InnerException properties. 28 | /// 29 | public SpriteParseException(string fileName, Exception innerException) 30 | : base("The sprite document contains errors that prevents it from generating an image sprite.", innerException) 31 | { 32 | FileName = fileName; 33 | } 34 | 35 | /// 36 | /// Serialization constructor 37 | /// 38 | protected SpriteParseException(SerializationInfo info, StreamingContext context) 39 | : base(info, context) 40 | { 41 | if (info != null) 42 | { 43 | FileName = info.GetString(nameof(FileName)); 44 | } 45 | } 46 | 47 | /// 48 | /// The name of the file containing the syntax error. 49 | /// 50 | public string FileName { get; } 51 | 52 | /// Serialization specific 53 | public override void GetObjectData(SerializationInfo info, StreamingContext context) 54 | { 55 | if (info != null) 56 | { 57 | info.AddValue(nameof(FileName), FileName); 58 | } 59 | 60 | base.GetObjectData(info, context); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ImageSprites/SpriteStylesheetGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace ImageSprites 10 | { 11 | internal class SpriteStylesheetGenerator 12 | { 13 | public async static Task ExportStylesheet(IEnumerable fragments, SpriteDocument doc, SpriteImageGenerator generator) 14 | { 15 | if (doc.Stylesheet == Stylesheet.None) 16 | return; 17 | 18 | var mainClass = SpriteHelpers.GetIdentifier(doc.FileName); 19 | 20 | string outputFile = doc.FileName + "." + doc.Stylesheet.ToString().ToLowerInvariant(); 21 | var outputDirectory = Path.GetDirectoryName(outputFile); 22 | var bgUrl = doc.PathPrefix + SpriteHelpers.MakeRelative(outputFile, doc.FileName + doc.OutputExtension); 23 | if (doc.AppendCacheBustSpriteSuffix) 24 | { 25 | using (HashAlgorithm sha = new SHA256Managed()) 26 | { 27 | byte[] spriteContent = File.ReadAllBytes(doc.FileName + doc.OutputExtension); 28 | string spriteHash = Convert.ToBase64String(sha.ComputeHash(spriteContent)); 29 | bgUrl += $"?hash={WebUtility.UrlEncode(spriteHash)}"; 30 | } 31 | } 32 | 33 | StringBuilder sb = new StringBuilder(GetDescription(doc.Stylesheet)); 34 | 35 | if (doc.Stylesheet == Stylesheet.Css) 36 | { 37 | sb.AppendLine($".{mainClass} {{"); 38 | sb.AppendLine($"\tbackground-image: url('{bgUrl}');"); 39 | sb.AppendLine($"\tbackground-repeat: no-repeat;"); 40 | AddCustomStyles(doc.CustomStyles, sb); 41 | sb.AppendLine("}"); 42 | } 43 | 44 | foreach (SpriteFragment fragment in fragments) 45 | { 46 | if (doc.Stylesheet == Stylesheet.Css) 47 | { 48 | sb.AppendLine($"{GetSelector(fragment.ID, mainClass, doc.Stylesheet)} {{"); 49 | sb.AppendLine($"\twidth: {fragment.Width}px;"); 50 | sb.AppendLine($"\theight: {fragment.Height}px;"); 51 | sb.AppendLine($"\tbackground-position: -{fragment.X}px -{fragment.Y}px;"); 52 | sb.AppendLine("}"); 53 | } 54 | else if (doc.Stylesheet == Stylesheet.Styl) 55 | { 56 | sb.AppendLine(GetSelector(fragment.ID, mainClass, doc.Stylesheet)); 57 | sb.AppendLine($"\twidth {fragment.Width}px"); 58 | sb.AppendLine($"\theigh {fragment.Height}px"); 59 | sb.AppendLine($"\tbackground-position -{fragment.X}px -{fragment.Y}px"); 60 | } 61 | else 62 | { 63 | sb.AppendLine(GetSelector(fragment.ID, mainClass, doc.Stylesheet) + " {"); 64 | sb.AppendLine($"\twidth: {fragment.Width}px;"); 65 | sb.AppendLine($"\theight: {fragment.Height}px;"); 66 | AddCustomStyles(doc.CustomStyles, sb); 67 | sb.AppendLine($"\tbackground: url('{bgUrl}') -{fragment.X}px -{fragment.Y}px no-repeat;"); 68 | sb.AppendLine("}"); 69 | } 70 | } 71 | 72 | if (File.Exists(outputFile) && File.ReadAllText(outputFile) == sb.ToString()) 73 | return; 74 | 75 | Directory.CreateDirectory(outputDirectory); 76 | 77 | using (var writer = new StreamWriter(outputFile)) 78 | { 79 | generator.OnSaving(outputFile, doc); 80 | await writer.WriteAsync(sb.ToString().Replace("-0px", "0")); 81 | generator.OnSaved(outputFile, doc); 82 | } 83 | } 84 | 85 | private static void AddCustomStyles(IDictionary customStyles, StringBuilder sb) 86 | { 87 | foreach (string property in customStyles.Keys) 88 | { 89 | sb.AppendLine($"\t{property}: {customStyles[property]};"); 90 | } 91 | } 92 | 93 | private static string GetDescription(Stylesheet format) 94 | { 95 | string text = "This is an example of how to use the image sprite in your own CSS files"; 96 | 97 | if (format != Stylesheet.Css) 98 | text = "@import this file directly into your existing " + format + " files to use these mixins"; 99 | 100 | return "/*" + Environment.NewLine + text + Environment.NewLine + "*/" + Environment.NewLine; 101 | } 102 | 103 | private static string GetSelector(string ident, string mainClass, Stylesheet format) 104 | { 105 | switch (format) 106 | { 107 | case Stylesheet.Less: 108 | return $".{mainClass}-{ident}()"; 109 | case Stylesheet.Scss: 110 | return $"@mixin {mainClass}-{ident}()"; 111 | case Stylesheet.Styl: 112 | return $"{mainClass}-{ident}()"; 113 | default: // CSS 114 | return $".{mainClass}.{ident}"; 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/ImageSprites/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Adornments/AdornmentLayer.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using Microsoft.VisualStudio.Text.Editor; 3 | using Microsoft.VisualStudio.Utilities; 4 | 5 | namespace ImageSpritesVsix 6 | { 7 | class AdornmentLayer 8 | { 9 | public const string LayerName = "Sprite Logo"; 10 | 11 | [Export(typeof(AdornmentLayerDefinition))] 12 | [Name(LayerName)] 13 | [Order(Before = PredefinedAdornmentLayers.Caret)] 14 | public AdornmentLayerDefinition editorAdornmentLayer = null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Adornments/AdornmentProvider.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using System.IO; 3 | using Microsoft.VisualStudio.Settings; 4 | using Microsoft.VisualStudio.Shell; 5 | using Microsoft.VisualStudio.Shell.Settings; 6 | using Microsoft.VisualStudio.Text; 7 | using Microsoft.VisualStudio.Text.Editor; 8 | using Microsoft.VisualStudio.Utilities; 9 | 10 | namespace ImageSpritesVsix 11 | { 12 | [Export(typeof(IWpfTextViewCreationListener))] 13 | [ContentType(SpriteContentTypeDefinition.SpriteContentType)] 14 | [TextViewRole(PredefinedTextViewRoles.Document)] 15 | class AdornmentProvider : IWpfTextViewCreationListener 16 | { 17 | private const string _propertyName = "ShowWatermark"; 18 | private const double _initOpacity = 0.3D; 19 | SettingsManager _settingsManager; 20 | 21 | private static bool _isVisible, _hasLoaded; 22 | 23 | [Import] 24 | public ITextDocumentFactoryService TextDocumentFactoryService { get; set; } 25 | 26 | [Import] 27 | public SVsServiceProvider serviceProvider { get; set; } 28 | 29 | private void LoadSettings() 30 | { 31 | _hasLoaded = true; 32 | 33 | _settingsManager = new ShellSettingsManager(serviceProvider); 34 | SettingsStore store = _settingsManager.GetReadOnlySettingsStore(SettingsScope.UserSettings); 35 | 36 | LogoAdornment.VisibilityChanged += AdornmentVisibilityChanged; 37 | 38 | _isVisible = store.GetBoolean(Vsix.Name, _propertyName, true); 39 | } 40 | 41 | private void AdornmentVisibilityChanged(object sender, bool isVisible) 42 | { 43 | WritableSettingsStore wstore = _settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings); 44 | _isVisible = isVisible; 45 | 46 | if (!wstore.CollectionExists(Vsix.Name)) 47 | wstore.CreateCollection(Vsix.Name); 48 | 49 | wstore.SetBoolean(Vsix.Name, _propertyName, isVisible); 50 | } 51 | 52 | public void TextViewCreated(IWpfTextView textView) 53 | { 54 | if (!_hasLoaded) 55 | LoadSettings(); 56 | 57 | ITextDocument document; 58 | if (TextDocumentFactoryService.TryGetTextDocument(textView.TextDataModel.DocumentBuffer, out document)) 59 | { 60 | string fileName = Path.GetFileName(document.FilePath).ToLowerInvariant(); 61 | 62 | // Check if filename is absolute because when debugging, script files are sometimes dynamically created. 63 | if (string.IsNullOrEmpty(fileName) || !Path.IsPathRooted(document.FilePath)) 64 | return; 65 | 66 | CreateAdornments(document, textView); 67 | } 68 | } 69 | 70 | private void CreateAdornments(ITextDocument document, IWpfTextView textView) 71 | { 72 | textView.Properties.GetOrCreateSingletonProperty(() => new LogoAdornment(textView, _isVisible, _initOpacity)); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Adornments/LogoAdornment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Windows.Controls; 5 | using System.Windows.Media; 6 | using System.Windows.Media.Imaging; 7 | using Microsoft.VisualStudio.Text.Editor; 8 | 9 | namespace ImageSpritesVsix 10 | { 11 | class LogoAdornment 12 | { 13 | private IAdornmentLayer _adornmentLayer; 14 | private Image _adornment; 15 | private readonly double _initOpacity; 16 | private double _currentOpacity; 17 | 18 | public LogoAdornment(IWpfTextView view, bool isVisible, double initOpacity) 19 | { 20 | _adornmentLayer = view.GetAdornmentLayer(AdornmentLayer.LayerName); 21 | _currentOpacity = isVisible ? initOpacity : 0; 22 | _initOpacity = initOpacity; 23 | 24 | CreateImage(); 25 | 26 | view.ViewportHeightChanged += SetAdornmentLocation; 27 | view.ViewportWidthChanged += SetAdornmentLocation; 28 | VisibilityChanged += ToggleVisibility; 29 | 30 | if (_adornmentLayer.IsEmpty) 31 | _adornmentLayer.AddAdornment(AdornmentPositioningBehavior.ViewportRelative, null, null, _adornment, null); 32 | } 33 | 34 | private void ToggleVisibility(object sender, bool isVisible) 35 | { 36 | _adornment.Opacity = isVisible ? _initOpacity : 0; 37 | _currentOpacity = _adornment.Opacity; 38 | } 39 | 40 | private void CreateImage() 41 | { 42 | _adornment = new Image(); 43 | _adornment.Source = GetImage(); 44 | _adornment.ToolTip = "Click to toggle visibility"; 45 | _adornment.Opacity = _currentOpacity; 46 | _adornment.SetValue(RenderOptions.BitmapScalingModeProperty, BitmapScalingMode.HighQuality); 47 | 48 | _adornment.MouseEnter += (s, e) => { _adornment.Opacity = 1D; }; 49 | _adornment.MouseLeave += (s, e) => { _adornment.Opacity = _currentOpacity; }; 50 | _adornment.MouseLeftButtonUp += (s, e) => { OnVisibilityChanged(_currentOpacity == 0); }; 51 | } 52 | 53 | private static ImageSource GetImage() 54 | { 55 | string assembly = Assembly.GetExecutingAssembly().Location; 56 | string folder = Path.GetDirectoryName(assembly); 57 | string file = Path.Combine(folder, "Resources\\icon.png"); 58 | 59 | Uri url = new Uri(file, UriKind.Absolute); 60 | return BitmapFrame.Create(url); 61 | } 62 | 63 | private void SetAdornmentLocation(object sender, EventArgs e) 64 | { 65 | IWpfTextView view = (IWpfTextView)sender; 66 | Canvas.SetLeft(_adornment, view.ViewportRight - _adornment.Source.Width - 20); 67 | Canvas.SetTop(_adornment, view.ViewportBottom - _adornment.Source.Height - 20); 68 | } 69 | 70 | public static event EventHandler VisibilityChanged; 71 | 72 | internal static void OnVisibilityChanged(bool isVisible) 73 | { 74 | VisibilityChanged?.Invoke(null, isVisible); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Commands/CreateSpriteCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Design; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Windows.Forms; 7 | using ImageSprites; 8 | using Microsoft.VisualStudio.Shell; 9 | 10 | namespace ImageSpritesVsix 11 | { 12 | internal sealed class CreateSpriteCommand 13 | { 14 | private CreateSpriteCommand(OleMenuCommandService commandService) 15 | { 16 | var id = new CommandID(PackageGuids.guidImageSpriteCmdSet, PackageIds.CreateSprite); 17 | var cmd = new OleMenuCommand(Execute, id); 18 | cmd.BeforeQueryStatus += BeforeQueryStatus; 19 | commandService.AddCommand(cmd); 20 | } 21 | 22 | public static CreateSpriteCommand Instance { get; private set; } 23 | 24 | public static void Initialize(OleMenuCommandService commandService) 25 | { 26 | Instance = new CreateSpriteCommand(commandService); 27 | } 28 | 29 | private void BeforeQueryStatus(object sender, EventArgs e) 30 | { 31 | var button = (OleMenuCommand)sender; 32 | IEnumerable files = GetFiles(); 33 | 34 | button.Enabled = button.Visible = files.Any(); 35 | } 36 | 37 | private void Execute(object sender, EventArgs e) 38 | { 39 | IEnumerable files = GetFiles(); 40 | string folder = Path.GetDirectoryName(files.First()); 41 | 42 | if (GetFileName(folder, out string spriteFile)) 43 | { 44 | var doc = new SpriteDocument(spriteFile, files) 45 | { 46 | Stylesheet = Stylesheet.Css 47 | }; 48 | 49 | ThreadHelper.JoinableTaskFactory.Run(async () => 50 | { 51 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 52 | 53 | await doc.Save(); 54 | ProjectHelpers.DTE.ItemOperations.OpenFile(doc.FileName); 55 | await SpriteService.GenerateSpriteAsync(doc); 56 | }); 57 | } 58 | } 59 | 60 | private bool GetFileName(string initialDirectory, out string fileName) 61 | { 62 | fileName = null; 63 | 64 | using (var dialog = new SaveFileDialog()) 65 | { 66 | dialog.InitialDirectory = initialDirectory; 67 | dialog.FileName = "mysprite" + Constants.FileExtension; 68 | dialog.DefaultExt = Constants.FileExtension; 69 | dialog.Filter = "Sprite files | *" + Constants.FileExtension; 70 | 71 | if (dialog.ShowDialog() != DialogResult.OK) 72 | { 73 | return false; 74 | } 75 | 76 | fileName = dialog.FileName; 77 | } 78 | 79 | return true; 80 | } 81 | 82 | private IEnumerable GetFiles() 83 | { 84 | return ProjectHelpers.GetSelectedItemPaths().Where(file => Constants.SupporedExtensions.Contains(Path.GetExtension(file))); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Commands/SpriteCreationListener.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using Microsoft.VisualStudio.Editor; 3 | using Microsoft.VisualStudio.Text; 4 | using Microsoft.VisualStudio.Text.Editor; 5 | using Microsoft.VisualStudio.TextManager.Interop; 6 | using Microsoft.VisualStudio.Utilities; 7 | 8 | namespace ImageSpritesVsix 9 | { 10 | [Export(typeof(IVsTextViewCreationListener))] 11 | [ContentType(SpriteContentTypeDefinition.SpriteContentType)] 12 | [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] 13 | class SpriteCreationListener : IVsTextViewCreationListener 14 | { 15 | private static bool _hasReloadedSchemas; 16 | 17 | [Import] 18 | public IVsEditorAdaptersFactoryService EditorAdaptersFactoryService { get; set; } 19 | 20 | [Import] 21 | public ITextDocumentFactoryService TextDocumentFactoryService { get; set; } 22 | 23 | public void VsTextViewCreated(IVsTextView textViewAdapter) 24 | { 25 | IWpfTextView textView = EditorAdaptersFactoryService.GetWpfTextView(textViewAdapter); 26 | ITextDocument doc; 27 | 28 | if (TextDocumentFactoryService.TryGetTextDocument(textView.TextDataModel.DocumentBuffer, out doc)) 29 | { 30 | doc.FileActionOccurred += DocumentSaved; 31 | 32 | if (!_hasReloadedSchemas) 33 | { 34 | ProjectHelpers.ExecuteCommand("OtherContextMenus.JSONContext.ReloadSchemas"); 35 | _hasReloadedSchemas = true; 36 | } 37 | } 38 | } 39 | 40 | private void DocumentSaved(object sender, TextDocumentFileActionEventArgs e) 41 | { 42 | if (e.FileActionType == FileActionTypes.ContentSavedToDisk) 43 | { 44 | SpriteService.GenerateSpriteAsync(e.FilePath).ConfigureAwait(false); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Commands/UpdateAllSpritesCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.Design; 4 | using System.IO; 5 | using EnvDTE; 6 | using EnvDTE80; 7 | using Microsoft.VisualStudio.Shell; 8 | 9 | namespace ImageSpritesVsix 10 | { 11 | internal sealed class UpdateAllSpritesCommand 12 | { 13 | private Project _project; 14 | private Solution2 _solution; 15 | 16 | private UpdateAllSpritesCommand(OleMenuCommandService commandService) 17 | { 18 | var id = new CommandID(PackageGuids.guidImageSpriteCmdSet, PackageIds.UpdateAllSprite); 19 | var cmd = new OleMenuCommand(Execute, id); 20 | cmd.BeforeQueryStatus += BeforeQueryStatus; 21 | commandService.AddCommand(cmd); 22 | } 23 | 24 | public static UpdateAllSpritesCommand Instance { get; private set; } 25 | 26 | public static void Initialize(OleMenuCommandService commandService) 27 | { 28 | Instance = new UpdateAllSpritesCommand(commandService); 29 | } 30 | 31 | private void BeforeQueryStatus(object sender, EventArgs e) 32 | { 33 | var button = (OleMenuCommand)sender; 34 | 35 | button.Enabled = button.Visible = ProjectHelpers.GetProjectOrSolution(out _project, out _solution); 36 | } 37 | 38 | private async void Execute(object sender, EventArgs e) 39 | { 40 | string folder = _project != null ? _project.GetRootFolder() : Path.GetDirectoryName(_solution.FileName); 41 | 42 | if (!Directory.Exists(folder)) 43 | return; 44 | 45 | List files = GetFiles(folder, "*" + Constants.FileExtension); 46 | 47 | foreach (string file in files) 48 | { 49 | await SpriteService.GenerateSpriteAsync(file); 50 | } 51 | } 52 | 53 | private static List GetFiles(string path, string pattern) 54 | { 55 | var files = new List(); 56 | 57 | if (path.Contains("node_modules")) 58 | return files; 59 | 60 | try 61 | { 62 | files.AddRange(Directory.GetFiles(path, pattern, SearchOption.TopDirectoryOnly)); 63 | foreach (string directory in Directory.GetDirectories(path)) 64 | files.AddRange(GetFiles(directory, pattern)); 65 | } 66 | catch { } 67 | 68 | return files; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Commands/UpdateSpriteCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Design; 3 | using System.IO; 4 | using System.Linq; 5 | using Microsoft.VisualStudio.Shell; 6 | 7 | namespace ImageSpritesVsix 8 | { 9 | internal sealed class UpdateSpriteCommand 10 | { 11 | private static readonly string[] _allowd = { ".png", ".jpg", ".jpeg", ".gif" }; 12 | 13 | private UpdateSpriteCommand(OleMenuCommandService commandService) 14 | { 15 | var id = new CommandID(PackageGuids.guidImageSpriteCmdSet, PackageIds.UpdateSprite); 16 | var cmd = new OleMenuCommand(Execute, id); 17 | cmd.BeforeQueryStatus += BeforeQueryStatus; 18 | commandService.AddCommand(cmd); 19 | } 20 | 21 | public static UpdateSpriteCommand Instance { get; private set; } 22 | 23 | public static void Initialize(OleMenuCommandService commandService) 24 | { 25 | Instance = new UpdateSpriteCommand(commandService); 26 | } 27 | 28 | private void BeforeQueryStatus(object sender, EventArgs e) 29 | { 30 | var button = (OleMenuCommand)sender; 31 | System.Collections.Generic.IEnumerable files = ProjectHelpers.GetSelectedItemPaths(); 32 | 33 | bool isSprite = files.Count() == 1 && Path.GetExtension(files.First()).Equals(Constants.FileExtension, StringComparison.OrdinalIgnoreCase); 34 | 35 | button.Enabled = button.Visible = isSprite; 36 | } 37 | 38 | private async void Execute(object sender, EventArgs e) 39 | { 40 | System.Collections.Generic.IEnumerable files = ProjectHelpers.GetSelectedItemPaths(); 41 | await SpriteService.GenerateSpriteAsync(files.First()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace ImageSpritesVsix 2 | { 3 | static class Constants 4 | { 5 | public const string FileExtension = ".sprite"; 6 | public static readonly string[] SupporedExtensions = { ".png", ".jpg", ".jpeg", ".gif" }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/ContentType/SpriteContentTypeDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using Microsoft.VisualStudio.Utilities; 3 | 4 | namespace ImageSpritesVsix 5 | { 6 | class SpriteContentTypeDefinition 7 | { 8 | public const string SpriteContentType = "Sprite"; 9 | 10 | [Export(typeof(ContentTypeDefinition))] 11 | [Name(SpriteContentType)] 12 | [BaseDefinition("json")] 13 | public ContentTypeDefinition ISpriteContentTypeDefinitionContentType { get; set; } 14 | 15 | [Export(typeof(FileExtensionToContentTypeDefinition))] 16 | [ContentType(SpriteContentType)] 17 | [FileExtension(Constants.FileExtension)] 18 | public FileExtensionToContentTypeDefinition SpriteFileExtensionDefinition { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/ContentType/registry.pkgdef: -------------------------------------------------------------------------------- 1 | [$RootKey$\ShellFileAssociations\.sprite] 2 | "DefaultIconMoniker"="KnownMonikers.BuildQueue" 3 | 4 | // Website project file nesting 5 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.sprite] 6 | "RelationType"=dword:00000001 7 | 8 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.sprite\.sprite.png] 9 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.sprite\.sprite.gif] 10 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.sprite\.sprite.jpg] 11 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.sprite\.sprite.less] 12 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.sprite\.sprite.scss] 13 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.sprite\.sprite.css] 14 | [$RootKey$\Projects\{E24C65DC-7377-472b-9ABA-BC803B73C61A}\RelatedFiles\.sprite\.sprite.styl] -------------------------------------------------------------------------------- /src/ImageSpritesVsix/DragDrop/IgnoreDropHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Windows; 5 | using ImageSprites; 6 | using Microsoft.VisualStudio.Shell; 7 | using Microsoft.VisualStudio.Text.Editor; 8 | using Microsoft.VisualStudio.Text.Editor.DragDrop; 9 | 10 | namespace ImageSpritesVsix 11 | { 12 | internal class IgnoreDropHandler : IDropHandler 13 | { 14 | private IWpfTextView _view; 15 | private string _draggedFileName; 16 | private string _documentFileName; 17 | 18 | public IgnoreDropHandler(IWpfTextView view, string fileName) 19 | { 20 | _view = view; 21 | _documentFileName = fileName; 22 | } 23 | 24 | public DragDropPointerEffects HandleDataDropped(DragDropInfo dragDropInfo) 25 | { 26 | Microsoft.VisualStudio.Text.SnapshotPoint position = dragDropInfo.VirtualBufferPosition.Position; 27 | var doc = SpriteDocument.FromJSON(_view.TextBuffer.CurrentSnapshot.GetText(), _documentFileName); 28 | 29 | string ident = SpriteHelpers.GetIdentifier(_draggedFileName); 30 | string file = SpriteHelpers.MakeRelative(_documentFileName, _draggedFileName); 31 | 32 | if (doc.Images.ContainsKey(ident)) 33 | ident += "_" + Guid.NewGuid().ToString().Replace("-", string.Empty); 34 | 35 | doc.Images.Add(new KeyValuePair(ident, file)); 36 | 37 | using (Microsoft.VisualStudio.Text.ITextEdit edit = _view.TextBuffer.CreateEdit()) 38 | { 39 | edit.Replace(0, _view.TextBuffer.CurrentSnapshot.Length, doc.ToJsonString()); 40 | edit.Apply(); 41 | } 42 | 43 | return DragDropPointerEffects.Copy; 44 | } 45 | 46 | public void HandleDragCanceled() 47 | { } 48 | 49 | public DragDropPointerEffects HandleDragStarted(DragDropInfo dragDropInfo) 50 | { 51 | return DragDropPointerEffects.All; 52 | } 53 | 54 | public DragDropPointerEffects HandleDraggingOver(DragDropInfo dragDropInfo) 55 | { 56 | return DragDropPointerEffects.All; 57 | } 58 | 59 | public bool IsDropEnabled(DragDropInfo dragDropInfo) 60 | { 61 | _draggedFileName = GetImageFilename(dragDropInfo); 62 | 63 | return File.Exists(_draggedFileName); 64 | } 65 | 66 | private static string GetImageFilename(DragDropInfo info) 67 | { 68 | var data = new DataObject(info.Data); 69 | 70 | if (info.Data.GetDataPresent("FileDrop")) 71 | { 72 | // The drag and drop operation came from the file system 73 | System.Collections.Specialized.StringCollection files = data.GetFileDropList(); 74 | 75 | if (files != null && files.Count == 1) 76 | { 77 | return files[0]; 78 | } 79 | } 80 | else if (info.Data.GetDataPresent("CF_VSSTGPROJECTITEMS")) 81 | { 82 | // The drag and drop operation came from the VS solution explorer 83 | return data.GetText(); 84 | } 85 | 86 | return null; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/ImageSpritesVsix/DragDrop/IgnoreDropHandlerProvider.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Composition; 2 | using Microsoft.VisualStudio.Text; 3 | using Microsoft.VisualStudio.Text.Editor; 4 | using Microsoft.VisualStudio.Text.Editor.DragDrop; 5 | using Microsoft.VisualStudio.Utilities; 6 | 7 | namespace ImageSpritesVsix 8 | { 9 | [Export(typeof(IDropHandlerProvider))] 10 | [DropFormat("CF_VSSTGPROJECTITEMS")] 11 | [DropFormat("FileDrop")] 12 | [Name("IgnoreDropHandler")] 13 | [ContentType(SpriteContentTypeDefinition.SpriteContentType)] 14 | [Order(Before = "DefaultFileDropHandler")] 15 | internal class IgnoreDropHandlerProvider : IDropHandlerProvider 16 | { 17 | [Import] 18 | ITextDocumentFactoryService TextDocumentFactoryService { get; set; } 19 | 20 | public IDropHandler GetAssociatedDropHandler(IWpfTextView view) 21 | { 22 | ITextDocument document; 23 | 24 | if (TextDocumentFactoryService.TryGetTextDocument(view.TextBuffer, out document)) 25 | { 26 | return view.Properties.GetOrCreateSingletonProperty(() => new IgnoreDropHandler(view, document.FilePath)); 27 | } 28 | 29 | return null; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Extensions.vsext: -------------------------------------------------------------------------------- 1 | { 2 | "id": "15714ab6-b8ed-43b6-83d1-d4eb5de9eb5f", 3 | "name": "My Visual Studio extensions", 4 | "description": "Public Marketplace extensions that adds value to this extension", 5 | "version": "1.0", 6 | "extensions": [ 7 | { 8 | "name": "Image Optimizer", 9 | "vsixId": "bf95754f-93d3-42ff-bfe3-e05d23188b08" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Helpers/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.Shell; 3 | using Microsoft.VisualStudio.Shell.Interop; 4 | using task = System.Threading.Tasks.Task; 5 | 6 | internal static class Logger 7 | { 8 | private static string _name; 9 | private static IVsOutputWindowPane _pane; 10 | private static IVsOutputWindow _output; 11 | 12 | public static void Initialize(IServiceProvider provider, string name) 13 | { 14 | _output = (IVsOutputWindow)provider.GetService(typeof(SVsOutputWindow)); 15 | _name = name; 16 | } 17 | 18 | public static async task InitializeAsync(AsyncPackage package, string name) 19 | { 20 | _output = await package.GetServiceAsync(typeof(SVsOutputWindow)) as IVsOutputWindow; 21 | _name = name; 22 | } 23 | 24 | public static void Log(object message) 25 | { 26 | try 27 | { 28 | if (EnsurePane()) 29 | { 30 | _pane.OutputString(DateTime.Now.ToString() + ": " + message + Environment.NewLine); 31 | } 32 | } 33 | catch (Exception ex) 34 | { 35 | System.Diagnostics.Debug.Write(ex); 36 | } 37 | } 38 | 39 | private static bool EnsurePane() 40 | { 41 | if (_pane == null && _output != null) 42 | { 43 | ThreadHelper.Generic.BeginInvoke(() => 44 | { 45 | Guid guid = Guid.NewGuid(); 46 | _output.CreatePane(ref guid, _name, 1, 1); 47 | _output.GetPane(ref guid, out _pane); 48 | }); 49 | } 50 | 51 | return _pane != null; 52 | } 53 | } -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Helpers/ProjectHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using EnvDTE; 5 | using EnvDTE80; 6 | using Microsoft.VisualStudio.Shell; 7 | 8 | namespace ImageSpritesVsix 9 | { 10 | internal static class ProjectHelpers 11 | { 12 | static ProjectHelpers() 13 | { 14 | DTE = (DTE2)Package.GetGlobalService(typeof(DTE)); 15 | } 16 | 17 | public static DTE2 DTE { get; } 18 | 19 | public static void CheckFileOutOfSourceControl(string file) 20 | { 21 | if (!File.Exists(file) || DTE.Solution.FindProjectItem(file) == null) 22 | return; 23 | 24 | if (DTE.SourceControl.IsItemUnderSCC(file) && !DTE.SourceControl.IsItemCheckedOut(file)) 25 | DTE.SourceControl.CheckOutItem(file); 26 | 27 | var info = new FileInfo(file) 28 | { 29 | IsReadOnly = false 30 | }; 31 | } 32 | 33 | internal static void ExecuteCommand(string name, string argument = null) 34 | { 35 | try 36 | { 37 | Command command = DTE.Commands.Item(name); 38 | 39 | if (command != null && command.IsAvailable) 40 | { 41 | DTE.Commands.Raise(command.Guid, command.ID, argument, null); 42 | } 43 | } 44 | catch (Exception ex) 45 | { 46 | System.Diagnostics.Debug.Write(ex); 47 | } 48 | } 49 | 50 | public static IEnumerable GetSelectedItems() 51 | { 52 | var items = (Array)DTE.ToolWindows.SolutionExplorer.SelectedItems; 53 | 54 | foreach (UIHierarchyItem selItem in items) 55 | { 56 | ProjectItem item = selItem.Object as ProjectItem; 57 | 58 | if (item != null) 59 | yield return item; 60 | } 61 | } 62 | 63 | public static bool GetProjectOrSolution(out Project project, out Solution2 solution) 64 | { 65 | var items = (Array)DTE.ToolWindows.SolutionExplorer.SelectedItems; 66 | project = null; 67 | solution = null; 68 | 69 | foreach (UIHierarchyItem selItem in items) 70 | { 71 | var projItem = selItem.Object as Project; 72 | 73 | if (projItem != null) 74 | project = projItem; 75 | 76 | var solItem = selItem.Object as Solution2; 77 | 78 | if (solItem != null) 79 | solution = solItem; 80 | } 81 | 82 | return project != null || solution != null; 83 | } 84 | 85 | public static IEnumerable GetSelectedItemPaths() 86 | { 87 | foreach (ProjectItem item in GetSelectedItems()) 88 | { 89 | if (item != null && item.Properties != null) 90 | yield return item.Properties.Item("FullPath").Value.ToString(); 91 | } 92 | } 93 | 94 | public static string GetRootFolder(this Project project) 95 | { 96 | if (project == null || string.IsNullOrEmpty(project.FullName)) 97 | return null; 98 | 99 | string fullPath; 100 | 101 | try 102 | { 103 | fullPath = project.Properties.Item("FullPath").Value as string; 104 | } 105 | catch (ArgumentException) 106 | { 107 | try 108 | { 109 | // MFC projects don't have FullPath, and there seems to be no way to query existence 110 | fullPath = project.Properties.Item("ProjectDirectory").Value as string; 111 | } 112 | catch (ArgumentException) 113 | { 114 | // Installer projects have a ProjectPath. 115 | fullPath = project.Properties.Item("ProjectPath").Value as string; 116 | } 117 | } 118 | 119 | if (string.IsNullOrEmpty(fullPath)) 120 | return File.Exists(project.FullName) ? Path.GetDirectoryName(project.FullName) : null; 121 | 122 | if (Directory.Exists(fullPath)) 123 | return fullPath; 124 | 125 | if (File.Exists(fullPath)) 126 | return Path.GetDirectoryName(fullPath); 127 | 128 | return null; 129 | } 130 | 131 | public static void AddFileToProject(this Project project, string file, string itemType = null) 132 | { 133 | if (project.IsKind(ProjectTypes.ASPNET_5, ProjectTypes.DOTNET_Core)) 134 | return; 135 | 136 | if (DTE.Solution.FindProjectItem(file) == null) 137 | { 138 | ProjectItem item = project.ProjectItems.AddFromFile(file); 139 | item.SetItemType(itemType); 140 | } 141 | } 142 | 143 | public static void SetItemType(this ProjectItem item, string itemType) 144 | { 145 | try 146 | { 147 | if (item == null || item.ContainingProject == null) 148 | return; 149 | 150 | if (string.IsNullOrEmpty(itemType) 151 | || item.ContainingProject.IsKind(ProjectTypes.WEBSITE_PROJECT) 152 | || item.ContainingProject.IsKind(ProjectTypes.UNIVERSAL_APP)) 153 | return; 154 | 155 | item.Properties.Item("ItemType").Value = itemType; 156 | } 157 | catch (Exception ex) 158 | { 159 | Logger.Log(ex); 160 | } 161 | } 162 | 163 | public static void AddNestedFile(string parentFile, string newFile, string itemType = null) 164 | { 165 | ProjectItem item = DTE.Solution.FindProjectItem(parentFile); 166 | 167 | try 168 | { 169 | if (item == null 170 | || item.ContainingProject == null 171 | || item.ContainingProject.IsKind(ProjectTypes.ASPNET_5)) 172 | return; 173 | 174 | if (item.ProjectItems == null || item.ContainingProject.IsKind(ProjectTypes.UNIVERSAL_APP)) 175 | { 176 | item.ContainingProject.AddFileToProject(newFile); 177 | } 178 | else if (DTE.Solution.FindProjectItem(newFile) == null) 179 | { 180 | item.ProjectItems.AddFromFile(newFile); 181 | } 182 | 183 | ProjectItem newItem = DTE.Solution.FindProjectItem(newFile); 184 | newItem.SetItemType(itemType); 185 | } 186 | catch (Exception ex) 187 | { 188 | Logger.Log(ex); 189 | } 190 | } 191 | 192 | public static bool IsKind(this Project project, params string[] kindGuids) 193 | { 194 | foreach (string guid in kindGuids) 195 | { 196 | if (project.Kind.Equals(guid, StringComparison.OrdinalIgnoreCase)) 197 | return true; 198 | } 199 | 200 | return false; 201 | } 202 | 203 | public static void DeleteFileFromProject(string file) 204 | { 205 | ProjectItem item = DTE.Solution.FindProjectItem(file); 206 | 207 | if (item == null) 208 | return; 209 | try 210 | { 211 | item.Delete(); 212 | } 213 | catch (Exception ex) 214 | { 215 | Logger.Log(ex); 216 | } 217 | } 218 | 219 | public static Project GetActiveProject() 220 | { 221 | try 222 | { 223 | Window2 window = DTE.ActiveWindow as Window2; 224 | Document doc = DTE.ActiveDocument; 225 | 226 | if (window != null && window.Type == vsWindowType.vsWindowTypeDocument) 227 | { 228 | // if a document is active, use the document's containing directory 229 | if (doc != null && !string.IsNullOrEmpty(doc.FullName)) 230 | { 231 | ProjectItem docItem = DTE.Solution.FindProjectItem(doc.FullName); 232 | 233 | if (docItem != null && docItem.ContainingProject != null) 234 | return docItem.ContainingProject; 235 | } 236 | } 237 | 238 | Array activeSolutionProjects = DTE.ActiveSolutionProjects as Array; 239 | 240 | if (activeSolutionProjects != null && activeSolutionProjects.Length > 0) 241 | return activeSolutionProjects.GetValue(0) as Project; 242 | 243 | if (doc != null && !string.IsNullOrEmpty(doc.FullName)) 244 | { 245 | ProjectItem item = DTE.Solution?.FindProjectItem(doc.FullName); 246 | 247 | if (item != null) 248 | return item.ContainingProject; 249 | } 250 | } 251 | catch (Exception ex) 252 | { 253 | Logger.Log("Error getting the active project" + ex); 254 | } 255 | 256 | return null; 257 | } 258 | 259 | public static void SelectInSolutionExplorer(string selected) 260 | { 261 | UIHierarchy solutionExplorer = (UIHierarchy)DTE.Windows.Item(EnvDTE.Constants.vsext_wk_SProjectWindow).Object; 262 | UIHierarchyItem rootNode = solutionExplorer.UIHierarchyItems.Item(1); 263 | 264 | Stack> parents = new Stack>(); 265 | ProjectItem targetItem = DTE.Solution.FindProjectItem(selected); 266 | 267 | if (targetItem == null) 268 | { 269 | return; 270 | } 271 | 272 | UIHierarchyItems collection = rootNode.UIHierarchyItems; 273 | int cursor = 1; 274 | bool oldExpand = collection.Expanded; 275 | 276 | while (cursor <= collection.Count || parents.Count > 0) 277 | { 278 | while (cursor > collection.Count && parents.Count > 0) 279 | { 280 | collection.Expanded = oldExpand; 281 | Tuple parent = parents.Pop(); 282 | collection = parent.Item1; 283 | cursor = parent.Item2; 284 | oldExpand = parent.Item3; 285 | } 286 | 287 | if (cursor > collection.Count) 288 | { 289 | break; 290 | } 291 | 292 | UIHierarchyItem result = collection.Item(cursor); 293 | ProjectItem item = result.Object as ProjectItem; 294 | 295 | if (item == targetItem) 296 | { 297 | result.Select(vsUISelectionType.vsUISelectionTypeSelect); 298 | return; 299 | } 300 | 301 | ++cursor; 302 | 303 | bool oldOldExpand = oldExpand; 304 | oldExpand = result.UIHierarchyItems.Expanded; 305 | result.UIHierarchyItems.Expanded = true; 306 | if (result.UIHierarchyItems.Count > 0) 307 | { 308 | parents.Push(Tuple.Create(collection, cursor, oldOldExpand)); 309 | collection = result.UIHierarchyItems; 310 | cursor = 1; 311 | } 312 | } 313 | } 314 | 315 | public static class ProjectTypes 316 | { 317 | public const string ASPNET_5 = "{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}"; 318 | public const string DOTNET_Core = "{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"; 319 | public const string WEBSITE_PROJECT = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}"; 320 | public const string UNIVERSAL_APP = "{262852C6-CD72-467D-83FE-5EEB1973A190}"; 321 | public const string NODE_JS = "{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}"; 322 | } 323 | } 324 | } -------------------------------------------------------------------------------- /src/ImageSpritesVsix/ImageSpriteCommandTable.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace ImageSpritesVsix 7 | { 8 | using System; 9 | 10 | /// 11 | /// Helper class that exposes all GUIDs used across VS Package. 12 | /// 13 | internal sealed partial class PackageGuids 14 | { 15 | public const string guidPackageString = "0c4c1075-3865-436a-925a-7b97a641c9e0"; 16 | public static Guid guidPackage = new Guid(guidPackageString); 17 | 18 | public const string guidImageSpriteCmdSetString = "ad408e80-5054-4184-b141-830702b6346c"; 19 | public static Guid guidImageSpriteCmdSet = new Guid(guidImageSpriteCmdSetString); 20 | } 21 | /// 22 | /// Helper class that encapsulates all CommandIDs uses across VS Package. 23 | /// 24 | internal sealed partial class PackageIds 25 | { 26 | public const int MenuGroup = 0x1020; 27 | public const int CreateSprite = 0x0100; 28 | public const int UpdateSprite = 0x0200; 29 | public const int UpdateAllSprite = 0x0300; 30 | } 31 | } -------------------------------------------------------------------------------- /src/ImageSpritesVsix/ImageSpriteCommandTable.vsct: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 37 | 38 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/ImageSpritePackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Design; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using Microsoft.VisualStudio.Shell; 6 | using Microsoft.VisualStudio.Shell.Interop; 7 | using task = System.Threading.Tasks.Task; 8 | 9 | namespace ImageSpritesVsix 10 | { 11 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 12 | [InstalledProductRegistration("#110", "#112", Vsix.Version, IconResourceID = 400)] 13 | [ProvideMenuResource("Menus.ctmenu", 1)] 14 | [ProvideAutoLoad(UIContextGuids80.SolutionHasSingleProject, PackageAutoLoadFlags.BackgroundLoad)] 15 | [ProvideAutoLoad(UIContextGuids80.SolutionHasMultipleProjects, PackageAutoLoadFlags.BackgroundLoad)] 16 | [Guid(PackageGuids.guidPackageString)] 17 | internal sealed class ImageSpritePackage : AsyncPackage 18 | { 19 | protected override async task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 20 | { 21 | await JoinableTaskFactory.SwitchToMainThreadAsync(); 22 | 23 | await Logger.InitializeAsync(this, Vsix.Name); 24 | var commandService = await GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 25 | 26 | CreateSpriteCommand.Initialize(commandService); 27 | UpdateSpriteCommand.Initialize(commandService); 28 | UpdateAllSpritesCommand.Initialize(commandService); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/ImageSpritesVsix.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(VisualStudioVersion) 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | true 7 | 8 | 9 | 10 | Program 11 | $(DevEnvDir)\devenv.exe 12 | /rootsuffix Exp 13 | 14 | 15 | 16 | Debug 17 | AnyCPU 18 | 2.0 19 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 20 | {D6AC6895-FAB6-4B0A-96AE-4FC1EFBB758E} 21 | Library 22 | Properties 23 | ImageSpritesVsix 24 | ImageSpritesVsix 25 | v4.8 26 | true 27 | true 28 | true 29 | true 30 | true 31 | false 32 | 33 | 34 | true 35 | full 36 | false 37 | bin\Debug\ 38 | DEBUG;TRACE 39 | prompt 40 | 4 41 | false 42 | BasicDesignGuidelineRules.ruleset 43 | 44 | 45 | pdbonly 46 | true 47 | bin\Release\ 48 | TRACE 49 | prompt 50 | 4 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | True 68 | True 69 | ImageSpriteCommandTable.vsct 70 | 71 | 72 | 73 | 74 | True 75 | True 76 | source.extension.vsixmanifest 77 | 78 | 79 | 80 | 81 | 82 | Resources\LICENSE 83 | true 84 | 85 | 86 | true 87 | 88 | 89 | Designer 90 | VsixManifestGenerator 91 | source.extension.cs 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Menus.ctmenu 105 | VsctGenerator 106 | ImageSpriteCommandTable.cs 107 | 108 | 109 | 110 | 111 | true 112 | 113 | 114 | true 115 | 116 | 117 | true 118 | 119 | 120 | 121 | 122 | {AB5AE1E7-8362-4A97-A8F9-3939BE8470C4} 123 | ImageSprites 124 | 125 | 126 | 127 | 128 | 17.0.0-previews-4-31709-430 129 | 130 | 131 | 17.0.5232 132 | runtime; build; native; contentfiles; analyzers 133 | all 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 150 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using ImageSpritesVsix; 4 | 5 | [assembly: AssemblyTitle(Vsix.Name)] 6 | [assembly: AssemblyDescription(Vsix.Description)] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany(Vsix.Author)] 9 | [assembly: AssemblyProduct(Vsix.Name)] 10 | [assembly: AssemblyCopyright(Vsix.Author)] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture(Vsix.Language)] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: AssemblyVersion(Vsix.Version)] 17 | [assembly: AssemblyFileVersion(Vsix.Version)] 18 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/src/ImageSpritesVsix/Resources/Icon.png -------------------------------------------------------------------------------- /src/ImageSpritesVsix/Resources/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/src/ImageSpritesVsix/Resources/Preview.png -------------------------------------------------------------------------------- /src/ImageSpritesVsix/SpriteService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Windows.Forms; 6 | using ImageSprites; 7 | 8 | namespace ImageSpritesVsix 9 | { 10 | class SpriteService 11 | { 12 | private static SpriteImageGenerator _generator; 13 | 14 | static SpriteService() 15 | { 16 | SpriteDocument.Saving += SpriteSaving; 17 | SpriteDocument.Saved += SpriteSaved; 18 | 19 | _generator = new SpriteImageGenerator(); 20 | _generator.Saving += SpriteImageSaving; 21 | _generator.Saved += SpriteImageSaved; 22 | } 23 | 24 | private static void SpriteImageSaved(object sender, SpriteImageGenerationEventArgs e) 25 | { 26 | ProjectHelpers.AddNestedFile(e.Document.FileName, e.FileName); 27 | OptimizeImage(e.FileName, e.Document.Optimize); 28 | } 29 | 30 | private static void OptimizeImage(string fileName, Optimization optimization) 31 | { 32 | try 33 | { 34 | string ext = Path.GetExtension(fileName); 35 | 36 | if (optimization != Optimization.None && Constants.SupporedExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase)) 37 | { 38 | string cmd = "ImageOptimizer.OptimizeLossless"; 39 | 40 | if (optimization == Optimization.Lossy) 41 | cmd = "ImageOptimizer.OptimizeLossy"; 42 | 43 | ProjectHelpers.ExecuteCommand(cmd, fileName); 44 | } 45 | } 46 | catch (Exception ex) 47 | { 48 | Logger.Log(ex); 49 | } 50 | } 51 | 52 | private static void SpriteImageSaving(object sender, SpriteImageGenerationEventArgs e) 53 | { 54 | ProjectHelpers.CheckFileOutOfSourceControl(e.FileName); 55 | } 56 | 57 | private static void SpriteSaved(object sender, FileSystemEventArgs e) 58 | { 59 | EnvDTE.Project project = ProjectHelpers.GetActiveProject(); 60 | 61 | if (project != null) 62 | { 63 | ProjectHelpers.AddFileToProject(project, e.FullPath, "None"); 64 | } 65 | } 66 | 67 | private static void SpriteSaving(object sender, FileSystemEventArgs e) 68 | { 69 | if (e.ChangeType == WatcherChangeTypes.Changed) 70 | { 71 | ProjectHelpers.CheckFileOutOfSourceControl(e.FullPath); 72 | } 73 | } 74 | 75 | public static async Task GenerateSpriteAsync(string fileName) 76 | { 77 | try 78 | { 79 | SpriteDocument doc = await SpriteDocument.FromFile(fileName); 80 | await GenerateSpriteAsync(doc); 81 | } 82 | catch (SpriteParseException ex) 83 | { 84 | MessageBox.Show(ex.Message, Vsix.Name, MessageBoxButtons.OK, MessageBoxIcon.Error); 85 | ProjectHelpers.DTE.ItemOperations.OpenFile(fileName); 86 | } 87 | catch (Exception ex) 88 | { 89 | Logger.Log(ex); 90 | } 91 | } 92 | 93 | public static async Task GenerateSpriteAsync(SpriteDocument doc) 94 | { 95 | try 96 | { 97 | await _generator.Generate(doc); 98 | } 99 | catch (FileNotFoundException ex) 100 | { 101 | MessageBox.Show(ex.Message, Vsix.Name, MessageBoxButtons.OK, MessageBoxIcon.Error); 102 | ProjectHelpers.DTE.ItemOperations.OpenFile(doc.FileName); 103 | } 104 | catch (Exception ex) 105 | { 106 | Logger.Log(ex); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/source.extension.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // This file was generated by VSIX Synchronizer 4 | // 5 | // ------------------------------------------------------------------------------ 6 | namespace ImageSpritesVsix 7 | { 8 | internal sealed partial class Vsix 9 | { 10 | public const string Id = "4974d92e-e0ac-462a-ae65-73a000503802"; 11 | public const string Name = "Image Sprites"; 12 | public const string Description = @"Boost your website's performance by creating image sprites to reduce the amount of HTTP requests needed."; 13 | public const string Language = "en-US"; 14 | public const string Version = "1.5"; 15 | public const string Author = "Mads Kristensen"; 16 | public const string Tags = "image, sprite, jpg, gif, png, css, stylesheet, stylus, less, sass, scss"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ImageSpritesVsix/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Image Sprites 6 | Boost your website's performance by creating image sprites to reduce the amount of HTTP requests needed. 7 | https://github.com/madskristensen/ImageSprites 8 | Resources\LICENSE 9 | https://github.com/madskristensen/ImageSprites/blob/master/CHANGELOG.md 10 | Resources\Icon.png 11 | Resources\Preview.png 12 | image, sprite, jpg, gif, png, css, stylesheet, stylus, less, sass, scss 13 | 14 | 15 | 16 | amd64 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/384dpi/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/384dpi/a.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/384dpi/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/384dpi/b.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/384dpi/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/384dpi/c.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/384dpi/d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/384dpi/d.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/384dpi/e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/384dpi/e.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/384dpi/f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/384dpi/f.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/96dpi/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/96dpi/a.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/96dpi/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/96dpi/b.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/96dpi/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/96dpi/c.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/96dpi/d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/96dpi/d.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/96dpi/e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/96dpi/e.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/Images/96dpi/f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madskristensen/ImageSprites/fae64735510a0d864d1d570f48db98ed3e885dc3/test/ImageSpritesTest/Artifacts/Images/96dpi/f.png -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/hash.sprite: -------------------------------------------------------------------------------- 1 | { 2 | "images": { 3 | "a": "images/96dpi/a.png", 4 | "b": "images/96dpi/b.png", 5 | "c": "images/96dpi/c.png", 6 | "d": "images/96dpi/d.png" 7 | }, 8 | "orientation": "horizontal", 9 | "padding": 20, 10 | "output": "jpg", 11 | "stylesheet": "css", 12 | "customstyles": { 13 | "display": "block", 14 | "margin": 0 15 | }, 16 | "appendcachebustspritesuffix": "true" 17 | } -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/png384.sprite: -------------------------------------------------------------------------------- 1 | { 2 | "images": { 3 | "a": "images/384dpi/a.png", 4 | "b": "images/384dpi/b.png", 5 | "c": "images/384dpi/c.png", 6 | "d": "images/384dpi/d.png", 7 | "e": "images/384dpi/e.png", 8 | "f": "images/384dpi/f.png" 9 | }, 10 | "dpi": 384, 11 | "stylesheet": "css" 12 | } -------------------------------------------------------------------------------- /test/ImageSpritesTest/Artifacts/png96.sprite: -------------------------------------------------------------------------------- 1 | { 2 | "images": { 3 | "a": "images/96dpi/a.png", 4 | "b": "images/96dpi/b.png", 5 | "c": "images/96dpi/c.png", 6 | "d": "images/96dpi/d.png", 7 | "e": "images/96dpi/e.png", 8 | "f": "images/96dpi/f.png" 9 | }, 10 | "orientation": "horizontal", 11 | "padding": 20, 12 | "output": "jpg", 13 | "stylesheet": "less", 14 | "customstyles": { 15 | "display": "block", 16 | "margin": 0 17 | } 18 | } -------------------------------------------------------------------------------- /test/ImageSpritesTest/GenerateTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Drawing; 4 | using System.IO; 5 | using System.Net; 6 | using System.Security.Cryptography; 7 | using System.Threading.Tasks; 8 | using ImageSprites; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace ImageSpritesTest 12 | { 13 | [TestClass] 14 | public class GenerateTest 15 | { 16 | private string _artifacts; 17 | private SpriteImageGenerator _generator; 18 | 19 | [TestInitialize] 20 | public void Setup() 21 | { 22 | _artifacts = new DirectoryInfo(@"..\..\Artifacts").FullName; 23 | _generator = new SpriteImageGenerator(); 24 | _generator.Saving += SavingEventHandler; 25 | _generator.Saved += SavingEventHandler; 26 | } 27 | 28 | private void SavingEventHandler(object sender, SpriteImageGenerationEventArgs e) 29 | { 30 | Assert.IsTrue(e.Document.Images.Any()); 31 | Assert.IsTrue(e.FileName.StartsWith(e.Document.FileName)); 32 | } 33 | 34 | [TestMethod] 35 | public async Task Png96Dpi() 36 | { 37 | var fileName = Path.Combine(_artifacts, "png96.sprite"); 38 | var imgFile = fileName + ".jpg"; 39 | var lessFile = fileName + ".less"; 40 | 41 | try 42 | { 43 | var doc = await SpriteDocument.FromFile(fileName); 44 | await _generator.Generate(doc); 45 | 46 | using (var image = Image.FromFile(imgFile)) 47 | { 48 | Assert.AreEqual(56, image.Height); // 16 + padding 49 | Assert.AreEqual(236, image.Width); // 16 * 6 + padding 50 | } 51 | 52 | string less = File.ReadAllText(lessFile); 53 | Assert.IsTrue(less.Contains(".png96-a()"), "Sprite \"a.png\" not generated"); 54 | Assert.IsTrue(less.Contains("url('png96.sprite.jpg')"), "Incorrect url value"); 55 | Assert.IsTrue(less.Contains("margin: 0"), "Incorrect custom style"); 56 | } 57 | finally 58 | { 59 | File.Delete(imgFile); 60 | File.Delete(lessFile); 61 | } 62 | } 63 | 64 | [TestMethod] 65 | public async Task Png384Dpi() 66 | { 67 | var fileName = Path.Combine(_artifacts, "png384.sprite"); 68 | var imgFile = fileName + ".png"; 69 | var cssFile = fileName + ".css"; 70 | 71 | try 72 | { 73 | var doc = await SpriteDocument.FromFile(fileName); 74 | await _generator.Generate(doc); 75 | 76 | using (var image = Image.FromFile(imgFile)) 77 | { 78 | Assert.AreEqual(384, Math.Round(image.HorizontalResolution), "Not 384 DPI"); 79 | Assert.AreEqual(166, image.Height); // 16 + padding 80 | Assert.AreEqual(36, image.Width); // 16 * 6 + padding 81 | } 82 | 83 | string css = File.ReadAllText(cssFile); 84 | Assert.IsTrue(css.Contains(".png384.a"), "Sprite \"a.png\" not generated"); 85 | Assert.IsTrue(css.Contains("url('png384.sprite.png')"), "Incorrect url value"); 86 | Assert.IsTrue(css.Contains("display: inline-block"), "Incorrect custom style"); 87 | } 88 | finally 89 | { 90 | File.Delete(imgFile); 91 | File.Delete(cssFile); 92 | } 93 | } 94 | 95 | [TestMethod] 96 | public async Task CacheBustHash() 97 | { 98 | var fileName = Path.Combine(_artifacts, "hash.sprite"); 99 | var imgFile = fileName + ".jpg"; 100 | var cssFile = fileName + ".css"; 101 | 102 | try 103 | { 104 | var doc = await SpriteDocument.FromFile(fileName); 105 | await _generator.Generate(doc); 106 | 107 | using (HashAlgorithm hash = new SHA256Managed()) 108 | { 109 | string imgHash = Convert.ToBase64String(hash.ComputeHash(File.ReadAllBytes(imgFile))); 110 | string css = File.ReadAllText(cssFile); 111 | Assert.IsTrue(css.Contains($"?hash={WebUtility.UrlEncode(imgHash)}"), "Sprite hash not generated"); 112 | } 113 | } 114 | finally 115 | { 116 | File.Delete(imgFile); 117 | File.Delete(cssFile); 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/ImageSpritesTest/ImageSpritesTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | {6A23F2E8-DEFF-48AB-8844-D55D811229C7} 7 | Library 8 | Properties 9 | ImageSpritesTest 10 | ImageSpritesTest 11 | v4.5.1 12 | 512 13 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 14 | 10.0 15 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 16 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 17 | False 18 | UnitTest 19 | 20 | 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | 29 | 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | ..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll 40 | True 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {AB5AE1E7-8362-4A97-A8F9-3939BE8470C4} 65 | ImageSprites 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 | False 94 | 95 | 96 | False 97 | 98 | 99 | False 100 | 101 | 102 | False 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 121 | -------------------------------------------------------------------------------- /test/ImageSpritesTest/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ImageSpritesTest")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ImageSpritesTest")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("6a23f2e8-deff-48ab-8844-d55d811229c7")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /test/ImageSpritesTest/SerializationTest.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using ImageSprites; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | namespace ImageSpritesTest 8 | { 9 | [TestClass] 10 | public class SerializationTest 11 | { 12 | private string _fileName; 13 | private string _artifacts; 14 | 15 | [TestInitialize] 16 | public void Setup() 17 | { 18 | _artifacts = new DirectoryInfo(@"..\..\Artifacts").FullName; 19 | _fileName = Path.Combine(_artifacts, "file.sprite"); 20 | 21 | SpriteDocument.Saving += SavingEventHandler; 22 | SpriteDocument.Saved += SavingEventHandler; 23 | } 24 | 25 | private void SavingEventHandler(object sender, FileSystemEventArgs e) 26 | { 27 | Assert.IsTrue(e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType == WatcherChangeTypes.Created); 28 | } 29 | 30 | [TestCleanup] 31 | public void Cleanup() 32 | { 33 | File.Delete(_fileName); 34 | } 35 | 36 | [TestMethod] 37 | public async Task CreateDocument() 38 | { 39 | var img = Path.Combine(_artifacts, "images/96dpi/a.png"); 40 | var original = new SpriteDocument(_fileName, new[] { img }); 41 | original.Output = ImageType.Jpg; 42 | await original.Save(); 43 | 44 | var read = await SpriteDocument.FromFile(_fileName); 45 | 46 | Assert.AreEqual(original.Orientation, read.Orientation); 47 | Assert.AreEqual(original.FileName, read.FileName); 48 | Assert.AreEqual(original.Output, read.Output); 49 | Assert.AreEqual(original.Images.Count(), read.Images.Count()); 50 | 51 | var input = new FileInfo(Path.Combine(_artifacts, "images/96dpi/a.png")).FullName; 52 | Assert.AreEqual(input, read.ToAbsoluteImages().First().Value); 53 | } 54 | 55 | [TestMethod] 56 | public void FromJsonFail() 57 | { 58 | try 59 | { 60 | SpriteDocument.FromJSON("\"images\": {\"a\": \"dontexist.png\"}}", null); 61 | } 62 | catch (SpriteParseException ex) 63 | { 64 | Assert.IsNull(ex.FileName); 65 | } 66 | } 67 | 68 | [TestMethod, ExpectedException(typeof(FileNotFoundException))] 69 | public async Task FromFileFail() 70 | { 71 | await SpriteDocument.FromFile("doesntexist.json"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/ImageSpritesTest/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------