├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── alex.png ├── backbody.go ├── backbody_test.go ├── body.go ├── body_test.go ├── cmd └── main.go ├── extra ├── gradient-1.png ├── gradient-2.png └── gradient-3.png ├── face.go ├── face_test.go ├── frontbody.go ├── frontbody_test.go ├── go.mod ├── go.sum ├── head.go ├── head_test.go ├── leftbody.go ├── leftbody_test.go ├── matrix.go ├── options.go ├── parts.go ├── parts_test.go ├── rightbody.go ├── rightbody_test.go ├── steve.png └── util.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: "1.18" 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Test output 18 | backbody_steve_test_*.png 19 | backbody_alex_test_*.png 20 | face_steve_test_*.png 21 | face_alex_test_*.png 22 | frontbody_steve_test_*.png 23 | frontbody_alex_test_*.png 24 | fullbody_steve_test_*.png 25 | fullbody_alex_test_*.png 26 | head_steve_test_*.png 27 | head_alex_test_*.png 28 | leftbody_steve_test_*.png 29 | leftbody_alex_test_*.png 30 | rightbody_steve_test_*.png 31 | rightbody_alex_test_*.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mineatar.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test 3 | 4 | bench: 5 | go test -bench=. -count=1 -run=^$ -v 6 | 7 | clean: 8 | find . -name "*_*_test_*.png" -delete -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skin-render 2 | A fast and efficient library for rendering Minecraft skins into 2D and 3D isometric images. Inspired by existing services like [Crafatar](https://crafatar.com/) but with performance in mind. This library is used in production for [mineatar.io](https://mineatar.io). 3 | 4 | ## Documentation 5 | 6 | https://pkg.go.dev/github.com/mineatar-io/skin-render 7 | 8 | ## Examples 9 | 10 | Method | Result 11 | ------------------- | ------ 12 | `RenderFace()` | ![face_steve_test](https://api.mineatar.io/face/c06f89064c8a49119c29ea1dbd1aab82?scale=8) 13 | `RenderHead()` | ![head_steve_test](https://api.mineatar.io/head/c06f89064c8a49119c29ea1dbd1aab82?scale=6) 14 | `RenderBody()` | ![fullbody_steve_test](https://api.mineatar.io/body/full/c06f89064c8a49119c29ea1dbd1aab82?scale=6) 15 | `RenderFrontBody()` | ![frontbody_steve_test](https://api.mineatar.io/body/front/c06f89064c8a49119c29ea1dbd1aab82?scale=6) 16 | `RenderBackBody()` | ![backbody_steve_test](https://api.mineatar.io/body/back/c06f89064c8a49119c29ea1dbd1aab82?scale=6) 17 | `RenderLeftBody()` | ![leftbody_steve_test](https://api.mineatar.io/body/left/c06f89064c8a49119c29ea1dbd1aab82?scale=6) 18 | `RenderRightBody()` | ![rightbody_steve_test](https://api.mineatar.io/body/right/c06f89064c8a49119c29ea1dbd1aab82?scale=6) 19 | 20 | ## Credit 21 | 22 | - [Isometric graphics in Inkscape — Nicolás Guarín-Zapata](https://web.archive.org/web/20220306062006/https://nicoguaro.github.io/posts/isometric_inkscape/) 23 | - [go-gl/matgl](https://github.com/go-gl/mathgl) 24 | - [LapisBlue/Lapitar](https://github.com/LapisBlue/Lapitar) 25 | - [go/x/image `transform_Image_Image_Over` function](https://cs.opensource.google/go/x/image/+/refs/heads/master:draw/impl.go;drc=ed5dba0ea28f9438e4dac0320f7d9bb2fddd9737;l=965) 26 | - [go/x/image matrix `invert` function](https://cs.opensource.google/go/x/image/+/refs/heads/master:draw/scale.go;l=332;drc=ed5dba0ea28f9438e4dac0320f7d9bb2fddd9737) 27 | - And various other online matrix tutorials 28 | 29 | A special thanks to `oakar258` in the [Minecraft Wiki Discord server](https://minecraft.fandom.com/wiki/Minecraft_Wiki:Discord) for support on how Minecraft scales and renders the overlay skin layer. 30 | 31 | ## License 32 | 33 | [MIT License](https://github.com/mineatar-io/skin-render/blob/main/LICENSE) 34 | -------------------------------------------------------------------------------- /alex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mineatar-io/skin-render/16fa0fda282b061bad949032d6871167c9a1e8b1/alex.png -------------------------------------------------------------------------------- /backbody.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | // RenderBackBody renders a 2-dimensional image of the back of a Minecraft player's skin. 8 | func RenderBackBody(img image.Image, opts Options) *image.NRGBA { 9 | if err := validateSkin(img); err != nil { 10 | panic(err) 11 | } 12 | 13 | var ( 14 | skin *image.NRGBA = convertToNRGBA(img) 15 | slimOffset int = getSlimOffset(opts.Slim) 16 | isOldSkin bool = IsOldSkin(skin) 17 | output *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, 16, 32)) 18 | backHead *image.NRGBA = removeTransparency(extract(skin, HeadBack)) 19 | backTorso *image.NRGBA = removeTransparency(extract(skin, TorsoBack)) 20 | backLeftArm *image.NRGBA = nil 21 | backRightArm *image.NRGBA = removeTransparency(extract(skin, GetRightArmBack(opts.Slim))) 22 | backLeftLeg *image.NRGBA = nil 23 | backRightLeg *image.NRGBA = removeTransparency(extract(skin, RightLegBack)) 24 | ) 25 | 26 | if isOldSkin { 27 | backLeftArm = flipHorizontal(backRightArm) 28 | backLeftLeg = flipHorizontal(backRightLeg) 29 | } else { 30 | backLeftArm = removeTransparency(extract(skin, GetLeftArmBack(opts.Slim))) 31 | backLeftLeg = removeTransparency(extract(skin, LeftLegBack)) 32 | } 33 | 34 | if opts.Overlay { 35 | overlaySkin := fixTransparency(skin) 36 | 37 | composite(backHead, extract(overlaySkin, HeadOverlayBack), 0, 0) 38 | 39 | if !isOldSkin { 40 | composite(backTorso, extract(overlaySkin, TorsoOverlayBack), 0, 0) 41 | composite(backLeftArm, extract(overlaySkin, GetLeftArmOverlayBack(opts.Slim)), 0, 0) 42 | composite(backRightArm, extract(overlaySkin, GetRightArmOverlayBack(opts.Slim)), 0, 0) 43 | composite(backLeftLeg, extract(overlaySkin, LeftLegOverlayBack), 0, 0) 44 | composite(backRightLeg, extract(overlaySkin, RightLegOverlayBack), 0, 0) 45 | } 46 | } 47 | 48 | // Face 49 | composite(output, backHead, 4, 0) 50 | 51 | // Torso 52 | composite(output, backTorso, 4, 8) 53 | 54 | // Left Arm 55 | composite(output, backLeftArm, slimOffset, 8) 56 | 57 | // Right Arm 58 | composite(output, backRightArm, 12, 8) 59 | 60 | // Left Leg 61 | composite(output, backLeftLeg, 4, 20) 62 | 63 | // Right Leg 64 | composite(output, backRightLeg, 8, 20) 65 | 66 | if opts.Square { 67 | output = squareAndCenter(output) 68 | } 69 | 70 | return scale(output, opts.Scale) 71 | } 72 | -------------------------------------------------------------------------------- /backbody_test.go: -------------------------------------------------------------------------------- 1 | package skin_test 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "os" 7 | "testing" 8 | 9 | "github.com/mineatar-io/skin-render" 10 | ) 11 | 12 | func TestBackBodySteve(t *testing.T) { 13 | rawSkin := skin.GetDefaultSkin(false) 14 | 15 | for i := 0; i <= 8; i++ { 16 | scale := 1 << i 17 | 18 | output := skin.RenderBackBody(rawSkin, skin.Options{ 19 | Scale: scale, 20 | Overlay: true, 21 | Slim: false, 22 | Square: false, 23 | }) 24 | 25 | if output.Bounds().Dx() < 1 { 26 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 27 | } 28 | 29 | if output.Bounds().Dy() < 1 { 30 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 31 | } 32 | 33 | if writeRenders { 34 | f, err := os.OpenFile(fmt.Sprintf("backbody_steve_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 35 | 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if err = png.Encode(f, output); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if err = f.Close(); err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkBackBodySteve(b *testing.B) { 52 | rawSkin := skin.GetDefaultSkin(false) 53 | 54 | for n := 0; n <= b.N; n++ { 55 | skin.RenderBackBody(rawSkin, skin.Options{ 56 | Scale: defaultBenchmarkRenderScale, 57 | Overlay: true, 58 | Slim: false, 59 | Square: false, 60 | }) 61 | } 62 | } 63 | 64 | func TestBackBodyAlex(t *testing.T) { 65 | rawSkin := skin.GetDefaultSkin(true) 66 | 67 | for i := 0; i <= 8; i++ { 68 | scale := 1 << i 69 | 70 | output := skin.RenderBackBody(rawSkin, skin.Options{ 71 | Scale: scale, 72 | Overlay: true, 73 | Slim: true, 74 | Square: false, 75 | }) 76 | 77 | if output.Bounds().Dx() < 1 { 78 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 79 | } 80 | 81 | if output.Bounds().Dy() < 1 { 82 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 83 | } 84 | 85 | if writeRenders { 86 | f, err := os.OpenFile(fmt.Sprintf("backbody_alex_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 87 | 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if err = png.Encode(f, output); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if err = f.Close(); err != nil { 97 | t.Fatal(err) 98 | } 99 | } 100 | } 101 | } 102 | 103 | func BenchmarkBackBodyAlex(b *testing.B) { 104 | rawSkin := skin.GetDefaultSkin(true) 105 | 106 | for n := 0; n <= b.N; n++ { 107 | skin.RenderBackBody(rawSkin, skin.Options{ 108 | Scale: defaultBenchmarkRenderScale, 109 | Overlay: true, 110 | Slim: true, 111 | Square: false, 112 | }) 113 | } 114 | } 115 | 116 | func TestBackBodySteveSquare(t *testing.T) { 117 | rawSkin := skin.GetDefaultSkin(false) 118 | 119 | for i := 0; i <= 8; i++ { 120 | scale := 1 << i 121 | 122 | output := skin.RenderBackBody(rawSkin, skin.Options{ 123 | Scale: scale, 124 | Overlay: true, 125 | Slim: false, 126 | Square: true, 127 | }) 128 | 129 | if output.Bounds().Dx() < 1 { 130 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 131 | } 132 | 133 | if output.Bounds().Dy() < 1 { 134 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 135 | } 136 | 137 | if output.Bounds().Size().X != output.Bounds().Size().Y { 138 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 139 | } 140 | 141 | if writeRenders { 142 | f, err := os.OpenFile(fmt.Sprintf("backbody_steve_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 143 | 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | if err = png.Encode(f, output); err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | if err = f.Close(); err != nil { 153 | t.Fatal(err) 154 | } 155 | } 156 | } 157 | } 158 | 159 | func BenchmarkBackBodySteveSquare(b *testing.B) { 160 | rawSkin := skin.GetDefaultSkin(false) 161 | 162 | for n := 0; n <= b.N; n++ { 163 | skin.RenderBackBody(rawSkin, skin.Options{ 164 | Scale: defaultBenchmarkRenderScale, 165 | Overlay: true, 166 | Slim: false, 167 | Square: true, 168 | }) 169 | } 170 | } 171 | 172 | func TestBackBodyAlexSquare(t *testing.T) { 173 | rawSkin := skin.GetDefaultSkin(true) 174 | 175 | for i := 0; i <= 8; i++ { 176 | scale := 1 << i 177 | 178 | output := skin.RenderBackBody(rawSkin, skin.Options{ 179 | Scale: scale, 180 | Overlay: true, 181 | Slim: true, 182 | Square: true, 183 | }) 184 | 185 | if output.Bounds().Dx() < 1 { 186 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 187 | } 188 | 189 | if output.Bounds().Dy() < 1 { 190 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 191 | } 192 | 193 | if output.Bounds().Size().X != output.Bounds().Size().Y { 194 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 195 | } 196 | 197 | if writeRenders { 198 | f, err := os.OpenFile(fmt.Sprintf("backbody_alex_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 199 | 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | 204 | if err = png.Encode(f, output); err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | if err = f.Close(); err != nil { 209 | t.Fatal(err) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func BenchmarkBackBodyAlexSquare(b *testing.B) { 216 | rawSkin := skin.GetDefaultSkin(true) 217 | 218 | for n := 0; n <= b.N; n++ { 219 | skin.RenderBackBody(rawSkin, skin.Options{ 220 | Scale: defaultBenchmarkRenderScale, 221 | Overlay: true, 222 | Slim: true, 223 | Square: true, 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /body.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import ( 4 | "image" 5 | "math" 6 | ) 7 | 8 | // RenderBody renders a 3-dimensional image of the full body of a Minecraft player's skin. 9 | func RenderBody(img *image.NRGBA, opts Options) *image.NRGBA { 10 | if err := validateSkin(img); err != nil { 11 | panic(err) 12 | } 13 | 14 | var ( 15 | skin *image.NRGBA = convertToNRGBA(img) 16 | scaleDouble float64 = float64(opts.Scale) 17 | slimOffset int = getSlimOffset(opts.Slim) 18 | isOldSkin bool = IsOldSkin(skin) 19 | output *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, 17*opts.Scale+int(math.Ceil(scaleDouble*0.32)), 39*opts.Scale)) 20 | frontHead *image.NRGBA = removeTransparency(extract(skin, HeadFront)) 21 | topHead *image.NRGBA = rotate90(flipHorizontal(removeTransparency(extract(skin, HeadTop)))) 22 | rightHead *image.NRGBA = removeTransparency(extract(skin, HeadRight)) 23 | frontTorso *image.NRGBA = removeTransparency(extract(skin, TorsoFront)) 24 | frontLeftArm *image.NRGBA = nil 25 | topLeftArm *image.NRGBA = nil 26 | frontRightArm *image.NRGBA = removeTransparency(extract(skin, GetRightArmFront(opts.Slim))) 27 | topRightArm *image.NRGBA = removeTransparency(extract(skin, GetRightArmTop(opts.Slim))) 28 | rightRightArm *image.NRGBA = removeTransparency(extract(skin, RightArmRight)) 29 | frontLeftLeg *image.NRGBA = nil 30 | frontRightLeg *image.NRGBA = removeTransparency(extract(skin, RightLegFront)) 31 | rightRightLeg *image.NRGBA = removeTransparency(extract(skin, RightLegRight)) 32 | ) 33 | 34 | if isOldSkin { 35 | frontLeftArm = flipHorizontal(frontRightArm) 36 | topLeftArm = flipHorizontal(topRightArm) 37 | frontLeftLeg = flipHorizontal(frontRightLeg) 38 | } else { 39 | frontLeftArm = removeTransparency(extract(skin, GetLeftArmFront(opts.Slim))) 40 | topLeftArm = removeTransparency(extract(skin, GetLeftArmTop(opts.Slim))) 41 | frontLeftLeg = removeTransparency(extract(skin, LeftLegFront)) 42 | } 43 | 44 | if opts.Overlay { 45 | overlaySkin := fixTransparency(skin) 46 | 47 | composite(topHead, rotate90(flipHorizontal(extract(overlaySkin, HeadOverlayTop))), 0, 0) 48 | composite(frontHead, extract(overlaySkin, HeadOverlayFront), 0, 0) 49 | composite(rightHead, extract(overlaySkin, HeadOverlayRight), 0, 0) 50 | 51 | if !isOldSkin { 52 | composite(frontTorso, extract(overlaySkin, TorsoOverlayFront), 0, 0) 53 | composite(frontLeftArm, extract(overlaySkin, GetLeftArmOverlayFront(opts.Slim)), 0, 0) 54 | composite(topLeftArm, extract(overlaySkin, GetLeftArmOverlayTop(opts.Slim)), 0, 0) 55 | composite(frontRightArm, extract(overlaySkin, GetRightArmOverlayFront(opts.Slim)), 0, 0) 56 | composite(topRightArm, extract(overlaySkin, GetRightArmOverlayTop(opts.Slim)), 0, 0) 57 | composite(rightRightArm, extract(overlaySkin, RightArmOverlayRight), 0, 0) 58 | composite(frontLeftLeg, extract(overlaySkin, LeftLegOverlayFront), 0, 0) 59 | composite(frontRightLeg, extract(overlaySkin, RightLegOverlayFront), 0, 0) 60 | composite(rightRightLeg, extract(overlaySkin, RightLegOverlayRight), 0, 0) 61 | } 62 | } 63 | 64 | // Right Side of Right Leg 65 | compositeTransform(output, scale(rightRightLeg, opts.Scale), sideMatrix, 4*scaleDouble, 23*scaleDouble) 66 | 67 | // Front of Right Leg 68 | compositeTransform(output, scale(frontRightLeg, opts.Scale), frontMatrix, 8*scaleDouble, 31*scaleDouble) 69 | 70 | // Front of Left Leg 71 | compositeTransform(output, scale(frontLeftLeg, opts.Scale), frontMatrix, 12*scaleDouble, 31*scaleDouble) 72 | 73 | // Front of Torso 74 | compositeTransform(output, scale(frontTorso, opts.Scale), frontMatrix, 8*scaleDouble, 19*scaleDouble) 75 | 76 | // Front of Right Arm 77 | compositeTransform(output, scale(frontRightArm, opts.Scale), frontMatrix, float64(4+slimOffset)*scaleDouble, 19*scaleDouble) 78 | 79 | // Front of Left Arm 80 | compositeTransform(output, scale(frontLeftArm, opts.Scale), frontMatrix, 16*scaleDouble, 19*scaleDouble) 81 | 82 | // Top of Left Arm 83 | compositeTransform(output, scale(rotate270(topLeftArm), opts.Scale), plantMatrix, 15*scaleDouble, float64(slimOffset-1)*scaleDouble) 84 | 85 | // Right Side of Right Arm 86 | compositeTransform(output, scale(rightRightArm, opts.Scale), sideMatrix, float64(slimOffset)*scaleDouble, float64(15-slimOffset)*scaleDouble) 87 | 88 | // Top of Right Arm 89 | compositeTransform(output, scale(rotate90(topRightArm), opts.Scale), plantMatrix, 15*scaleDouble, 11*scaleDouble) 90 | 91 | // Front of Head 92 | compositeTransform(output, scale(frontHead, opts.Scale), frontMatrix, 10*scaleDouble, 13*scaleDouble) 93 | 94 | // Top of Head 95 | compositeTransform(output, scale(topHead, opts.Scale), plantMatrix, 5*scaleDouble, -5*scaleDouble) 96 | 97 | // Right Side of Head 98 | compositeTransform(output, scale(rightHead, opts.Scale), sideMatrix, 2*scaleDouble, 3*scaleDouble) 99 | 100 | if opts.Square { 101 | return squareAndCenter(output) 102 | } 103 | 104 | return output 105 | } 106 | -------------------------------------------------------------------------------- /body_test.go: -------------------------------------------------------------------------------- 1 | package skin_test 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "os" 7 | "testing" 8 | 9 | "github.com/mineatar-io/skin-render" 10 | ) 11 | 12 | func TestFullBodySteve(t *testing.T) { 13 | rawSkin := skin.GetDefaultSkin(false) 14 | 15 | for i := 0; i <= 8; i++ { 16 | scale := 1 << i 17 | 18 | output := skin.RenderBody(rawSkin, skin.Options{ 19 | Scale: scale, 20 | Overlay: true, 21 | Slim: false, 22 | Square: false, 23 | }) 24 | 25 | if output.Bounds().Dx() < 1 { 26 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 27 | } 28 | 29 | if output.Bounds().Dy() < 1 { 30 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 31 | } 32 | 33 | if writeRenders { 34 | f, err := os.OpenFile(fmt.Sprintf("fullbody_steve_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 35 | 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if err = png.Encode(f, output); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if err = f.Close(); err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkFullBodySteve(b *testing.B) { 52 | rawSkin := skin.GetDefaultSkin(false) 53 | 54 | for n := 0; n <= b.N; n++ { 55 | skin.RenderBody(rawSkin, skin.Options{ 56 | Scale: defaultBenchmarkRenderScale, 57 | Overlay: true, 58 | Slim: false, 59 | Square: false, 60 | }) 61 | } 62 | } 63 | 64 | func TestFullBodyAlex(t *testing.T) { 65 | rawSkin := skin.GetDefaultSkin(true) 66 | 67 | for i := 0; i <= 8; i++ { 68 | scale := 1 << i 69 | 70 | output := skin.RenderBody(rawSkin, skin.Options{ 71 | Scale: scale, 72 | Overlay: true, 73 | Slim: true, 74 | Square: false, 75 | }) 76 | 77 | if output.Bounds().Dx() < 1 { 78 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 79 | } 80 | 81 | if output.Bounds().Dy() < 1 { 82 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 83 | } 84 | 85 | if writeRenders { 86 | f, err := os.OpenFile(fmt.Sprintf("fullbody_alex_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 87 | 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if err = png.Encode(f, output); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if err = f.Close(); err != nil { 97 | t.Fatal(err) 98 | } 99 | } 100 | } 101 | } 102 | 103 | func BenchmarkFullBodyAlex(b *testing.B) { 104 | rawSkin := skin.GetDefaultSkin(true) 105 | 106 | for n := 0; n <= b.N; n++ { 107 | skin.RenderBody(rawSkin, skin.Options{ 108 | Scale: defaultBenchmarkRenderScale, 109 | Overlay: true, 110 | Slim: true, 111 | Square: false, 112 | }) 113 | } 114 | } 115 | 116 | func TestFullBodySteveSquare(t *testing.T) { 117 | rawSkin := skin.GetDefaultSkin(false) 118 | 119 | for i := 0; i <= 8; i++ { 120 | scale := 1 << i 121 | 122 | output := skin.RenderBody(rawSkin, skin.Options{ 123 | Scale: scale, 124 | Overlay: true, 125 | Slim: false, 126 | Square: true, 127 | }) 128 | 129 | if output.Bounds().Dx() < 1 { 130 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 131 | } 132 | 133 | if output.Bounds().Dy() < 1 { 134 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 135 | } 136 | 137 | if output.Bounds().Size().X != output.Bounds().Size().Y { 138 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 139 | } 140 | 141 | if writeRenders { 142 | f, err := os.OpenFile(fmt.Sprintf("fullbody_steve_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 143 | 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | if err = png.Encode(f, output); err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | if err = f.Close(); err != nil { 153 | t.Fatal(err) 154 | } 155 | } 156 | } 157 | } 158 | 159 | func BenchmarkFullBodySteveSquare(b *testing.B) { 160 | rawSkin := skin.GetDefaultSkin(false) 161 | 162 | for n := 0; n <= b.N; n++ { 163 | skin.RenderBody(rawSkin, skin.Options{ 164 | Scale: defaultBenchmarkRenderScale, 165 | Overlay: true, 166 | Slim: false, 167 | Square: true, 168 | }) 169 | } 170 | } 171 | 172 | func TestFullBodyAlexSquare(t *testing.T) { 173 | rawSkin := skin.GetDefaultSkin(true) 174 | 175 | for i := 0; i <= 8; i++ { 176 | scale := 1 << i 177 | 178 | output := skin.RenderBody(rawSkin, skin.Options{ 179 | Scale: scale, 180 | Overlay: true, 181 | Slim: true, 182 | Square: true, 183 | }) 184 | 185 | if output.Bounds().Dx() < 1 { 186 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 187 | } 188 | 189 | if output.Bounds().Dy() < 1 { 190 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 191 | } 192 | 193 | if output.Bounds().Size().X != output.Bounds().Size().Y { 194 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 195 | } 196 | 197 | if writeRenders { 198 | f, err := os.OpenFile(fmt.Sprintf("fullbody_alex_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 199 | 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | 204 | if err = png.Encode(f, output); err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | if err = f.Close(); err != nil { 209 | t.Fatal(err) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func BenchmarkFullBodyAlexSquare(b *testing.B) { 216 | rawSkin := skin.GetDefaultSkin(true) 217 | 218 | for n := 0; n <= b.N; n++ { 219 | skin.RenderBody(rawSkin, skin.Options{ 220 | Scale: defaultBenchmarkRenderScale, 221 | Overlay: true, 222 | Slim: true, 223 | Square: true, 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/draw" 7 | "image/png" 8 | "os" 9 | "time" 10 | 11 | "github.com/jessevdk/go-flags" 12 | "github.com/mineatar-io/skin-render" 13 | ) 14 | 15 | var ( 16 | inputFile string 17 | opts Options = Options{} 18 | ) 19 | 20 | type Options struct { 21 | Type string `short:"t" long:"type" description:"The type of image to render" default:"body"` 22 | Scale int `short:"s" long:"scale" description:"The scale of the rendered output image" default:"16"` 23 | NoOverlay bool `short:"O" long:"no-overlay" description:"Disables the overlay layer of the resulting image"` 24 | Slim bool `short:"S" long:"slim" description:"Enable this option if the input skin image is slim"` 25 | Square bool `long:"square" description:"Forces the output image to be square"` 26 | Output string `short:"o" long:"output" description:"The file to write the output image to" default:"output.png"` 27 | Verbose bool `short:"V" long:"verbose" description:"Prints extra debug information"` 28 | } 29 | 30 | func init() { 31 | args, err := flags.Parse(&opts) 32 | 33 | if err != nil { 34 | if flags.WroteHelp(err) { 35 | os.Exit(0) 36 | 37 | return 38 | } 39 | 40 | panic(err) 41 | } 42 | 43 | if len(args) < 1 { 44 | fmt.Println("missing input file argument") 45 | 46 | os.Exit(1) 47 | } 48 | 49 | inputFile = args[0] 50 | } 51 | 52 | func readInputFile() (*image.NRGBA, error) { 53 | f, err := os.Open(inputFile) 54 | 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | img, err := png.Decode(f) 60 | 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | if err = f.Close(); err != nil { 66 | return nil, err 67 | } 68 | 69 | output := image.NewNRGBA(img.Bounds()) 70 | draw.Draw(output, img.Bounds(), img, image.Pt(0, 0), draw.Src) 71 | 72 | return output, nil 73 | } 74 | 75 | func writeOutputImage(fileName string, img image.Image) error { 76 | f, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, 0777) 77 | 78 | if err != nil { 79 | return err 80 | } 81 | 82 | if err = png.Encode(f, img); err != nil { 83 | return err 84 | } 85 | 86 | return f.Close() 87 | } 88 | 89 | func main() { 90 | if opts.Verbose { 91 | fmt.Printf( 92 | "Using options:\n - Input: %s\n - Output: %s\n - Scale: %d\n - Overlay: %t\n - Slim: %t\n", 93 | inputFile, 94 | opts.Output, 95 | opts.Scale, 96 | !opts.NoOverlay, 97 | opts.Slim, 98 | ) 99 | } 100 | 101 | t := time.Now() 102 | 103 | skinImage, err := readInputFile() 104 | 105 | if err != nil { 106 | panic(err) 107 | } 108 | 109 | if opts.Verbose { 110 | fmt.Printf("Read input image (%s)\n", time.Since(t).Round(time.Microsecond)) 111 | } 112 | 113 | var ( 114 | result *image.NRGBA = nil 115 | options skin.Options = skin.Options{ 116 | Scale: opts.Scale, 117 | Overlay: !opts.NoOverlay, 118 | Slim: opts.Slim, 119 | Square: opts.Square, 120 | } 121 | ) 122 | 123 | t = time.Now() 124 | 125 | switch opts.Type { 126 | case "face": 127 | { 128 | result = skin.RenderFace(skinImage, options) 129 | 130 | break 131 | } 132 | case "head": 133 | { 134 | result = skin.RenderHead(skinImage, options) 135 | 136 | break 137 | } 138 | case "body": 139 | { 140 | result = skin.RenderBody(skinImage, options) 141 | 142 | break 143 | } 144 | case "front": 145 | { 146 | result = skin.RenderFrontBody(skinImage, options) 147 | 148 | break 149 | } 150 | case "back": 151 | { 152 | result = skin.RenderBackBody(skinImage, options) 153 | 154 | break 155 | } 156 | case "left": 157 | { 158 | result = skin.RenderLeftBody(skinImage, options) 159 | 160 | break 161 | } 162 | case "right": 163 | { 164 | result = skin.RenderRightBody(skinImage, options) 165 | 166 | break 167 | } 168 | default: 169 | { 170 | fmt.Printf("unknown --type value: %s\n", opts.Type) 171 | 172 | os.Exit(1) 173 | } 174 | } 175 | 176 | if opts.Verbose { 177 | fmt.Printf("Generated result image (%s)\n", time.Since(t).Round(time.Microsecond)) 178 | } 179 | 180 | t = time.Now() 181 | 182 | if err = writeOutputImage(opts.Output, result); err != nil { 183 | panic(err) 184 | } 185 | 186 | if opts.Verbose { 187 | fmt.Printf("Wrote output image (%s)\n", time.Since(t).Round(time.Microsecond)) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /extra/gradient-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mineatar-io/skin-render/16fa0fda282b061bad949032d6871167c9a1e8b1/extra/gradient-1.png -------------------------------------------------------------------------------- /extra/gradient-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mineatar-io/skin-render/16fa0fda282b061bad949032d6871167c9a1e8b1/extra/gradient-2.png -------------------------------------------------------------------------------- /extra/gradient-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mineatar-io/skin-render/16fa0fda282b061bad949032d6871167c9a1e8b1/extra/gradient-3.png -------------------------------------------------------------------------------- /face.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import "image" 4 | 5 | // RenderFace renders a 2-dimensional image of the face of a Minecraft player's skin. 6 | func RenderFace(img *image.NRGBA, opts Options) *image.NRGBA { 7 | if err := validateSkin(img); err != nil { 8 | panic(err) 9 | } 10 | 11 | var ( 12 | skin *image.NRGBA = convertToNRGBA(img) 13 | output *image.NRGBA = removeTransparency(extract(skin, HeadFront)) 14 | ) 15 | 16 | if opts.Overlay { 17 | overlaySkin := fixTransparency(skin) 18 | 19 | composite(output, extract(overlaySkin, HeadOverlayFront), 0, 0) 20 | } 21 | 22 | return scale(output, opts.Scale) 23 | } 24 | -------------------------------------------------------------------------------- /face_test.go: -------------------------------------------------------------------------------- 1 | package skin_test 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "os" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/mineatar-io/skin-render" 11 | ) 12 | 13 | var ( 14 | writeRenders = os.Getenv("WRITE_RENDERS") == "true" 15 | defaultBenchmarkRenderScale = 4 16 | ) 17 | 18 | func init() { 19 | if v := os.Getenv("RENDER_SCALE"); len(v) > 0 { 20 | value, err := strconv.ParseUint(v, 10, 32) 21 | 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | defaultBenchmarkRenderScale = int(value) 27 | } 28 | } 29 | 30 | func TestFaceSteve(t *testing.T) { 31 | rawSkin := skin.GetDefaultSkin(false) 32 | 33 | for i := 0; i <= 8; i++ { 34 | scale := 1 << i 35 | 36 | output := skin.RenderFace(rawSkin, skin.Options{ 37 | Scale: scale, 38 | Overlay: true, 39 | Slim: false, 40 | Square: false, 41 | }) 42 | 43 | if output.Bounds().Dx() < 1 { 44 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 45 | } 46 | 47 | if output.Bounds().Dy() < 1 { 48 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 49 | } 50 | 51 | if writeRenders { 52 | f, err := os.OpenFile(fmt.Sprintf("face_steve_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 53 | 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | if err = png.Encode(f, output); err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | if err = f.Close(); err != nil { 63 | t.Fatal(err) 64 | } 65 | } 66 | } 67 | } 68 | 69 | func BenchmarkFaceSteve(b *testing.B) { 70 | rawSkin := skin.GetDefaultSkin(false) 71 | 72 | for n := 0; n <= b.N; n++ { 73 | skin.RenderFace(rawSkin, skin.Options{ 74 | Scale: defaultBenchmarkRenderScale, 75 | Overlay: true, 76 | Slim: false, 77 | Square: false, 78 | }) 79 | } 80 | } 81 | 82 | func TestFaceAlex(t *testing.T) { 83 | rawSkin := skin.GetDefaultSkin(true) 84 | 85 | for i := 0; i <= 8; i++ { 86 | scale := 1 << i 87 | 88 | output := skin.RenderFace(rawSkin, skin.Options{ 89 | Scale: scale, 90 | Overlay: true, 91 | Slim: true, 92 | Square: false, 93 | }) 94 | 95 | if output.Bounds().Dx() < 1 { 96 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 97 | } 98 | 99 | if output.Bounds().Dy() < 1 { 100 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 101 | } 102 | 103 | if writeRenders { 104 | f, err := os.OpenFile(fmt.Sprintf("face_alex_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 105 | 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | if err = png.Encode(f, output); err != nil { 111 | t.Fatal(err) 112 | } 113 | 114 | if err = f.Close(); err != nil { 115 | t.Fatal(err) 116 | } 117 | } 118 | } 119 | } 120 | 121 | func BenchmarkFaceAlex(b *testing.B) { 122 | rawSkin := skin.GetDefaultSkin(true) 123 | 124 | for n := 0; n <= b.N; n++ { 125 | skin.RenderFace(rawSkin, skin.Options{ 126 | Scale: defaultBenchmarkRenderScale, 127 | Overlay: true, 128 | Slim: true, 129 | Square: false, 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /frontbody.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | // RenderFrontBody renders a 2-dimensional image of the front of a Minecraft player's skin. 8 | func RenderFrontBody(img *image.NRGBA, opts Options) *image.NRGBA { 9 | if err := validateSkin(img); err != nil { 10 | panic(err) 11 | } 12 | 13 | var ( 14 | skin *image.NRGBA = convertToNRGBA(img) 15 | slimOffset int = getSlimOffset(opts.Slim) 16 | isOldSkin bool = IsOldSkin(skin) 17 | output *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, 16, 32)) 18 | frontHead *image.NRGBA = removeTransparency(extract(skin, HeadFront)) 19 | frontTorso *image.NRGBA = removeTransparency(extract(skin, TorsoFront)) 20 | leftArm *image.NRGBA = nil 21 | rightArm *image.NRGBA = removeTransparency(extract(skin, GetRightArmFront(opts.Slim))) 22 | leftLeg *image.NRGBA = nil 23 | rightLeg *image.NRGBA = removeTransparency(extract(skin, RightLegFront)) 24 | ) 25 | 26 | if isOldSkin { 27 | leftArm = flipHorizontal(rightArm) 28 | leftLeg = flipHorizontal(rightLeg) 29 | } else { 30 | leftArm = removeTransparency(extract(skin, GetLeftArmFront(opts.Slim))) 31 | leftLeg = removeTransparency(extract(skin, LeftLegFront)) 32 | } 33 | 34 | if opts.Overlay { 35 | overlaySkin := fixTransparency(skin) 36 | 37 | composite(frontHead, extract(overlaySkin, HeadOverlayFront), 0, 0) 38 | 39 | if !isOldSkin { 40 | composite(frontTorso, extract(overlaySkin, TorsoOverlayFront), 0, 0) 41 | composite(leftArm, extract(overlaySkin, GetLeftArmOverlayFront(opts.Slim)), 0, 0) 42 | composite(rightArm, extract(overlaySkin, GetRightArmOverlayFront(opts.Slim)), 0, 0) 43 | composite(leftLeg, extract(overlaySkin, LeftLegOverlayFront), 0, 0) 44 | composite(rightLeg, extract(overlaySkin, RightLegOverlayFront), 0, 0) 45 | } 46 | } 47 | 48 | // Face 49 | composite(output, frontHead, 4, 0) 50 | 51 | // Torso 52 | composite(output, frontTorso, 4, 8) 53 | 54 | // Left Arm 55 | composite(output, leftArm, 12, 8) 56 | 57 | // Right Arm 58 | composite(output, rightArm, slimOffset, 8) 59 | 60 | // Left Leg 61 | composite(output, leftLeg, 8, 20) 62 | 63 | // Right Leg 64 | composite(output, rightLeg, 4, 20) 65 | 66 | if opts.Square { 67 | output = squareAndCenter(output) 68 | } 69 | 70 | return scale(output, opts.Scale) 71 | } 72 | -------------------------------------------------------------------------------- /frontbody_test.go: -------------------------------------------------------------------------------- 1 | package skin_test 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "os" 7 | "testing" 8 | 9 | "github.com/mineatar-io/skin-render" 10 | ) 11 | 12 | func TestFrontBodySteve(t *testing.T) { 13 | rawSkin := skin.GetDefaultSkin(false) 14 | 15 | for i := 0; i <= 8; i++ { 16 | scale := 1 << i 17 | 18 | output := skin.RenderFrontBody(rawSkin, skin.Options{ 19 | Scale: scale, 20 | Overlay: true, 21 | Slim: false, 22 | Square: false, 23 | }) 24 | 25 | if output.Bounds().Dx() < 1 { 26 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 27 | } 28 | 29 | if output.Bounds().Dy() < 1 { 30 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 31 | } 32 | 33 | if writeRenders { 34 | f, err := os.OpenFile(fmt.Sprintf("frontbody_steve_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 35 | 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if err = png.Encode(f, output); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if err = f.Close(); err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkFrontBodySteve(b *testing.B) { 52 | rawSkin := skin.GetDefaultSkin(false) 53 | 54 | for n := 0; n <= b.N; n++ { 55 | skin.RenderFrontBody(rawSkin, skin.Options{ 56 | Scale: defaultBenchmarkRenderScale, 57 | Overlay: true, 58 | Slim: false, 59 | Square: false, 60 | }) 61 | } 62 | } 63 | 64 | func TestFrontBodyAlex(t *testing.T) { 65 | rawSkin := skin.GetDefaultSkin(true) 66 | 67 | for i := 0; i <= 8; i++ { 68 | scale := 1 << i 69 | 70 | output := skin.RenderFrontBody(rawSkin, skin.Options{ 71 | Scale: scale, 72 | Overlay: true, 73 | Slim: true, 74 | Square: false, 75 | }) 76 | 77 | if output.Bounds().Dx() < 1 { 78 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 79 | } 80 | 81 | if output.Bounds().Dy() < 1 { 82 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 83 | } 84 | 85 | if writeRenders { 86 | f, err := os.OpenFile(fmt.Sprintf("frontbody_alex_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 87 | 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if err = png.Encode(f, output); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if err = f.Close(); err != nil { 97 | t.Fatal(err) 98 | } 99 | } 100 | } 101 | } 102 | 103 | func BenchmarkFrontBodyAlex(b *testing.B) { 104 | rawSkin := skin.GetDefaultSkin(true) 105 | 106 | for n := 0; n <= b.N; n++ { 107 | skin.RenderFrontBody(rawSkin, skin.Options{ 108 | Scale: defaultBenchmarkRenderScale, 109 | Overlay: true, 110 | Slim: true, 111 | Square: false, 112 | }) 113 | } 114 | } 115 | 116 | func TestFrontBodySteveSquare(t *testing.T) { 117 | rawSkin := skin.GetDefaultSkin(false) 118 | 119 | for i := 0; i <= 8; i++ { 120 | scale := 1 << i 121 | 122 | output := skin.RenderFrontBody(rawSkin, skin.Options{ 123 | Scale: scale, 124 | Overlay: true, 125 | Slim: false, 126 | Square: true, 127 | }) 128 | 129 | if output.Bounds().Dx() < 1 { 130 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 131 | } 132 | 133 | if output.Bounds().Dy() < 1 { 134 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 135 | } 136 | 137 | if output.Bounds().Size().X != output.Bounds().Size().Y { 138 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 139 | } 140 | 141 | if writeRenders { 142 | f, err := os.OpenFile(fmt.Sprintf("frontbody_steve_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 143 | 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | if err = png.Encode(f, output); err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | if err = f.Close(); err != nil { 153 | t.Fatal(err) 154 | } 155 | } 156 | } 157 | } 158 | 159 | func BenchmarkFrontBodySteveSquare(b *testing.B) { 160 | rawSkin := skin.GetDefaultSkin(false) 161 | 162 | for n := 0; n <= b.N; n++ { 163 | skin.RenderFrontBody(rawSkin, skin.Options{ 164 | Scale: defaultBenchmarkRenderScale, 165 | Overlay: true, 166 | Slim: false, 167 | Square: true, 168 | }) 169 | } 170 | } 171 | 172 | func TestFrontBodyAlexSquare(t *testing.T) { 173 | rawSkin := skin.GetDefaultSkin(true) 174 | 175 | for i := 0; i <= 8; i++ { 176 | scale := 1 << i 177 | 178 | output := skin.RenderFrontBody(rawSkin, skin.Options{ 179 | Scale: scale, 180 | Overlay: true, 181 | Slim: true, 182 | Square: true, 183 | }) 184 | 185 | if output.Bounds().Dx() < 1 { 186 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 187 | } 188 | 189 | if output.Bounds().Dy() < 1 { 190 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 191 | } 192 | 193 | if output.Bounds().Size().X != output.Bounds().Size().Y { 194 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 195 | } 196 | 197 | if writeRenders { 198 | f, err := os.OpenFile(fmt.Sprintf("frontbody_alex_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 199 | 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | 204 | if err = png.Encode(f, output); err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | if err = f.Close(); err != nil { 209 | t.Fatal(err) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func BenchmarkFrontBodyAlexSquare(b *testing.B) { 216 | rawSkin := skin.GetDefaultSkin(true) 217 | 218 | for n := 0; n <= b.N; n++ { 219 | skin.RenderFrontBody(rawSkin, skin.Options{ 220 | Scale: defaultBenchmarkRenderScale, 221 | Overlay: true, 222 | Slim: true, 223 | Square: true, 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mineatar-io/skin-render 2 | 3 | go 1.18 4 | 5 | require github.com/jessevdk/go-flags v1.5.0 6 | 7 | require golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 2 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 3 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= 4 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | -------------------------------------------------------------------------------- /head.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import ( 4 | "image" 5 | "math" 6 | ) 7 | 8 | // RenderHead renders a 3-dimensional image of the head of a Minecraft player's skin. 9 | func RenderHead(img *image.NRGBA, opts Options) *image.NRGBA { 10 | if err := validateSkin(img); err != nil { 11 | panic(err) 12 | } 13 | 14 | var ( 15 | skin *image.NRGBA = convertToNRGBA(img) 16 | scaleDouble float64 = float64(opts.Scale) 17 | output *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, 13*opts.Scale+int(math.Floor(scaleDouble*0.855)), 16*opts.Scale)) 18 | frontHead *image.NRGBA = removeTransparency(extract(skin, HeadFront)) 19 | topHead *image.NRGBA = rotate90(flipHorizontal(removeTransparency(extract(skin, HeadTop)))) 20 | rightHead *image.NRGBA = removeTransparency(extract(skin, HeadRight)) 21 | ) 22 | 23 | if opts.Overlay { 24 | overlaySkin := fixTransparency(skin) 25 | 26 | composite(frontHead, extract(overlaySkin, HeadOverlayFront), 0, 0) 27 | composite(topHead, rotate90(flipHorizontal(extract(overlaySkin, HeadOverlayTop))), 0, 0) 28 | composite(rightHead, extract(overlaySkin, HeadOverlayRight), 0, 0) 29 | } 30 | 31 | // Front Head 32 | compositeTransform(output, scale(frontHead, opts.Scale), frontMatrix, 8*scaleDouble, 12*scaleDouble) 33 | 34 | // Top Head 35 | compositeTransform(output, scale(topHead, opts.Scale), plantMatrix, 4*scaleDouble, -4*scaleDouble) 36 | 37 | // Right Head 38 | compositeTransform(output, scale(rightHead, opts.Scale), sideMatrix, 0, 4*scaleDouble) 39 | 40 | if opts.Square { 41 | return squareAndCenter(output) 42 | } 43 | 44 | return output 45 | } 46 | -------------------------------------------------------------------------------- /head_test.go: -------------------------------------------------------------------------------- 1 | package skin_test 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "os" 7 | "testing" 8 | 9 | "github.com/mineatar-io/skin-render" 10 | ) 11 | 12 | func TestHeadSteve(t *testing.T) { 13 | rawSkin := skin.GetDefaultSkin(false) 14 | 15 | for i := 0; i <= 8; i++ { 16 | scale := 1 << i 17 | 18 | output := skin.RenderHead(rawSkin, skin.Options{ 19 | Scale: scale, 20 | Overlay: true, 21 | Slim: false, 22 | Square: false, 23 | }) 24 | 25 | if output.Bounds().Dx() < 1 { 26 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 27 | } 28 | 29 | if output.Bounds().Dy() < 1 { 30 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 31 | } 32 | 33 | if writeRenders { 34 | f, err := os.OpenFile(fmt.Sprintf("head_steve_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 35 | 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if err = png.Encode(f, output); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if err = f.Close(); err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkHeadSteve(b *testing.B) { 52 | rawSkin := skin.GetDefaultSkin(false) 53 | 54 | for n := 0; n <= b.N; n++ { 55 | skin.RenderHead(rawSkin, skin.Options{ 56 | Scale: defaultBenchmarkRenderScale, 57 | Overlay: true, 58 | Slim: false, 59 | Square: false, 60 | }) 61 | } 62 | } 63 | 64 | func TestHeadAlex(t *testing.T) { 65 | rawSkin := skin.GetDefaultSkin(true) 66 | 67 | for i := 0; i <= 8; i++ { 68 | scale := 1 << i 69 | 70 | output := skin.RenderHead(rawSkin, skin.Options{ 71 | Scale: scale, 72 | Overlay: true, 73 | Slim: true, 74 | Square: false, 75 | }) 76 | 77 | if output.Bounds().Dx() < 1 { 78 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 79 | } 80 | 81 | if output.Bounds().Dy() < 1 { 82 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 83 | } 84 | 85 | if writeRenders { 86 | f, err := os.OpenFile(fmt.Sprintf("head_alex_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 87 | 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if err = png.Encode(f, output); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if err = f.Close(); err != nil { 97 | t.Fatal(err) 98 | } 99 | } 100 | } 101 | } 102 | 103 | func BenchmarkHeadAlex(b *testing.B) { 104 | rawSkin := skin.GetDefaultSkin(true) 105 | 106 | for n := 0; n <= b.N; n++ { 107 | skin.RenderHead(rawSkin, skin.Options{ 108 | Scale: defaultBenchmarkRenderScale, 109 | Overlay: true, 110 | Slim: true, 111 | Square: false, 112 | }) 113 | } 114 | } 115 | 116 | func TestHeadSteveSquare(t *testing.T) { 117 | rawSkin := skin.GetDefaultSkin(false) 118 | 119 | for i := 0; i <= 8; i++ { 120 | scale := 1 << i 121 | 122 | output := skin.RenderHead(rawSkin, skin.Options{ 123 | Scale: scale, 124 | Overlay: true, 125 | Slim: false, 126 | Square: true, 127 | }) 128 | 129 | if output.Bounds().Dx() < 1 { 130 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 131 | } 132 | 133 | if output.Bounds().Dy() < 1 { 134 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 135 | } 136 | 137 | if output.Bounds().Size().X != output.Bounds().Size().Y { 138 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 139 | } 140 | 141 | if writeRenders { 142 | f, err := os.OpenFile(fmt.Sprintf("head_steve_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 143 | 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | if err = png.Encode(f, output); err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | if err = f.Close(); err != nil { 153 | t.Fatal(err) 154 | } 155 | } 156 | } 157 | } 158 | 159 | func BenchmarkHeadSteveSquare(b *testing.B) { 160 | rawSkin := skin.GetDefaultSkin(false) 161 | 162 | for n := 0; n <= b.N; n++ { 163 | skin.RenderHead(rawSkin, skin.Options{ 164 | Scale: defaultBenchmarkRenderScale, 165 | Overlay: true, 166 | Slim: false, 167 | Square: true, 168 | }) 169 | } 170 | } 171 | 172 | func TestHeadAlexSquare(t *testing.T) { 173 | rawSkin := skin.GetDefaultSkin(true) 174 | 175 | for i := 0; i <= 8; i++ { 176 | scale := 1 << i 177 | 178 | output := skin.RenderHead(rawSkin, skin.Options{ 179 | Scale: scale, 180 | Overlay: true, 181 | Slim: true, 182 | Square: true, 183 | }) 184 | 185 | if output.Bounds().Dx() < 1 { 186 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 187 | } 188 | 189 | if output.Bounds().Dy() < 1 { 190 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 191 | } 192 | 193 | if output.Bounds().Size().X != output.Bounds().Size().Y { 194 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 195 | } 196 | 197 | if writeRenders { 198 | f, err := os.OpenFile(fmt.Sprintf("head_alex_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 199 | 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | 204 | if err = png.Encode(f, output); err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | if err = f.Close(); err != nil { 209 | t.Fatal(err) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func BenchmarkHeadAlexSquare(b *testing.B) { 216 | rawSkin := skin.GetDefaultSkin(true) 217 | 218 | for n := 0; n <= b.N; n++ { 219 | skin.RenderHead(rawSkin, skin.Options{ 220 | Scale: defaultBenchmarkRenderScale, 221 | Overlay: true, 222 | Slim: true, 223 | Square: true, 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /leftbody.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | // RenderLeftBody renders a 2-dimensional image of the left side of a Minecraft player's skin. 8 | func RenderLeftBody(img *image.NRGBA, opts Options) *image.NRGBA { 9 | if err := validateSkin(img); err != nil { 10 | panic(err) 11 | } 12 | 13 | var ( 14 | skin *image.NRGBA = convertToNRGBA(img) 15 | isOldSkin bool = IsOldSkin(skin) 16 | output *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, 16, 32)) 17 | leftHead *image.NRGBA = removeTransparency(extract(skin, HeadLeft)) 18 | leftLeftArm *image.NRGBA = nil 19 | leftLeftLeg *image.NRGBA = nil 20 | ) 21 | 22 | if isOldSkin { 23 | leftLeftArm = flipHorizontal(removeTransparency(extract(skin, GetRightArmLeft(false)))) 24 | leftLeftLeg = flipHorizontal(removeTransparency(extract(skin, RightLegLeft))) 25 | } else { 26 | leftLeftArm = removeTransparency(extract(skin, GetLeftArmLeft(opts.Slim))) 27 | leftLeftLeg = removeTransparency(extract(skin, LeftLegLeft)) 28 | } 29 | 30 | if opts.Overlay { 31 | overlaySkin := fixTransparency(skin) 32 | 33 | composite(leftHead, extract(overlaySkin, HeadOverlayLeft), 0, 0) 34 | 35 | if !isOldSkin { 36 | composite(leftLeftArm, extract(overlaySkin, GetLeftArmOverlayLeft(opts.Slim)), 0, 0) 37 | composite(leftLeftLeg, extract(overlaySkin, LeftLegOverlayLeft), 0, 0) 38 | } 39 | } 40 | 41 | // Left Head 42 | composite(output, leftHead, 4, 0) 43 | 44 | // Left Arm 45 | composite(output, leftLeftArm, 6, 8) 46 | 47 | // Left Leg 48 | composite(output, leftLeftLeg, 6, 20) 49 | 50 | if opts.Square { 51 | output = squareAndCenter(output) 52 | } 53 | 54 | return scale(output, opts.Scale) 55 | } 56 | -------------------------------------------------------------------------------- /leftbody_test.go: -------------------------------------------------------------------------------- 1 | package skin_test 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "os" 7 | "testing" 8 | 9 | "github.com/mineatar-io/skin-render" 10 | ) 11 | 12 | func TestLeftBodySteve(t *testing.T) { 13 | rawSkin := skin.GetDefaultSkin(false) 14 | 15 | for i := 0; i <= 8; i++ { 16 | scale := 1 << i 17 | 18 | output := skin.RenderLeftBody(rawSkin, skin.Options{ 19 | Scale: scale, 20 | Overlay: true, 21 | Slim: false, 22 | Square: false, 23 | }) 24 | 25 | if output.Bounds().Dx() < 1 { 26 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 27 | } 28 | 29 | if output.Bounds().Dy() < 1 { 30 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 31 | } 32 | 33 | if writeRenders { 34 | f, err := os.OpenFile(fmt.Sprintf("leftbody_steve_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 35 | 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if err = png.Encode(f, output); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if err = f.Close(); err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkLeftBodySteve(b *testing.B) { 52 | rawSkin := skin.GetDefaultSkin(false) 53 | 54 | for n := 0; n <= b.N; n++ { 55 | skin.RenderLeftBody(rawSkin, skin.Options{ 56 | Scale: defaultBenchmarkRenderScale, 57 | Overlay: true, 58 | Slim: false, 59 | Square: false, 60 | }) 61 | } 62 | } 63 | 64 | func TestLeftBodyAlex(t *testing.T) { 65 | rawSkin := skin.GetDefaultSkin(true) 66 | 67 | for i := 0; i <= 8; i++ { 68 | scale := 1 << i 69 | 70 | output := skin.RenderLeftBody(rawSkin, skin.Options{ 71 | Scale: scale, 72 | Overlay: true, 73 | Slim: true, 74 | Square: false, 75 | }) 76 | 77 | if output.Bounds().Dx() < 1 { 78 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 79 | } 80 | 81 | if output.Bounds().Dy() < 1 { 82 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 83 | } 84 | 85 | if writeRenders { 86 | f, err := os.OpenFile(fmt.Sprintf("leftbody_alex_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 87 | 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if err = png.Encode(f, output); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if err = f.Close(); err != nil { 97 | t.Fatal(err) 98 | } 99 | } 100 | } 101 | } 102 | 103 | func BenchmarkLeftBodyAlex(b *testing.B) { 104 | rawSkin := skin.GetDefaultSkin(true) 105 | 106 | for n := 0; n <= b.N; n++ { 107 | skin.RenderLeftBody(rawSkin, skin.Options{ 108 | Scale: defaultBenchmarkRenderScale, 109 | Overlay: true, 110 | Slim: true, 111 | Square: false, 112 | }) 113 | } 114 | } 115 | 116 | func TestLeftBodySteveSquare(t *testing.T) { 117 | rawSkin := skin.GetDefaultSkin(false) 118 | 119 | for i := 0; i <= 8; i++ { 120 | scale := 1 << i 121 | 122 | output := skin.RenderLeftBody(rawSkin, skin.Options{ 123 | Scale: scale, 124 | Overlay: true, 125 | Slim: false, 126 | Square: true, 127 | }) 128 | 129 | if output.Bounds().Dx() < 1 { 130 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 131 | } 132 | 133 | if output.Bounds().Dy() < 1 { 134 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 135 | } 136 | 137 | if output.Bounds().Size().X != output.Bounds().Size().Y { 138 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 139 | } 140 | 141 | if writeRenders { 142 | f, err := os.OpenFile(fmt.Sprintf("leftbody_steve_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 143 | 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | if err = png.Encode(f, output); err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | if err = f.Close(); err != nil { 153 | t.Fatal(err) 154 | } 155 | } 156 | } 157 | } 158 | 159 | func BenchmarkLeftBodySteveSquare(b *testing.B) { 160 | rawSkin := skin.GetDefaultSkin(false) 161 | 162 | for n := 0; n <= b.N; n++ { 163 | skin.RenderLeftBody(rawSkin, skin.Options{ 164 | Scale: defaultBenchmarkRenderScale, 165 | Overlay: true, 166 | Slim: false, 167 | Square: true, 168 | }) 169 | } 170 | } 171 | 172 | func TestLeftBodyAlexSquare(t *testing.T) { 173 | rawSkin := skin.GetDefaultSkin(true) 174 | 175 | for i := 0; i <= 8; i++ { 176 | scale := 1 << i 177 | 178 | output := skin.RenderLeftBody(rawSkin, skin.Options{ 179 | Scale: scale, 180 | Overlay: true, 181 | Slim: true, 182 | Square: true, 183 | }) 184 | 185 | if output.Bounds().Dx() < 1 { 186 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 187 | } 188 | 189 | if output.Bounds().Dy() < 1 { 190 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 191 | } 192 | 193 | if output.Bounds().Size().X != output.Bounds().Size().Y { 194 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 195 | } 196 | 197 | if writeRenders { 198 | f, err := os.OpenFile(fmt.Sprintf("leftbody_alex_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 199 | 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | 204 | if err = png.Encode(f, output); err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | if err = f.Close(); err != nil { 209 | t.Fatal(err) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func BenchmarkLeftBodyAlexSquare(b *testing.B) { 216 | rawSkin := skin.GetDefaultSkin(true) 217 | 218 | for n := 0; n <= b.N; n++ { 219 | skin.RenderLeftBody(rawSkin, skin.Options{ 220 | Scale: defaultBenchmarkRenderScale, 221 | Overlay: true, 222 | Slim: true, 223 | Square: true, 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /matrix.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import ( 4 | "image" 5 | "math" 6 | ) 7 | 8 | var ( 9 | sideMatrix matrix2x2 = rotationMatrix(degToRad(30)).Multiply(skewXMatrix(degToRad(30))).Multiply(scaleYMatrix(0.86603)) 10 | frontMatrix matrix2x2 = rotationMatrix(degToRad(-30)).Multiply(skewXMatrix(degToRad(-30))).Multiply(scaleYMatrix(0.86603)) 11 | plantMatrix matrix2x2 = rotationMatrix(degToRad(30)).Multiply(skewXMatrix(degToRad(-30))).Multiply(scaleYMatrix(0.86603)) 12 | ) 13 | 14 | type matrix2x2 [4]float64 15 | 16 | func (a matrix2x2) Multiply(b matrix2x2) matrix2x2 { 17 | return matrix2x2{ 18 | a[0]*b[0] + a[1]*b[2], 19 | a[0]*b[1] + a[1]*b[3], 20 | a[2]*b[0] + a[3]*b[2], 21 | a[2]*b[1] + a[3]*b[3], 22 | } 23 | } 24 | 25 | func (a matrix2x2) Determinant() float64 { 26 | return a[0]*a[3] - a[1]*a[2] 27 | } 28 | 29 | func (a matrix2x2) Inverse() matrix2x2 { 30 | d := a.Determinant() 31 | 32 | return matrix2x2{ 33 | a[3] * (1.0 / d), 34 | -a[1] * (1.0 / d), 35 | -a[2] * (1.0 / d), 36 | a[0] * (1.0 / d), 37 | } 38 | } 39 | 40 | func scaleYMatrix(a float64) matrix2x2 { 41 | return matrix2x2{ 42 | 1, 0, 43 | 0, a, 44 | } 45 | } 46 | 47 | func skewXMatrix(a float64) matrix2x2 { 48 | return matrix2x2{ 49 | 1, math.Tan(a), 50 | 0, 1, 51 | } 52 | } 53 | 54 | func rotationMatrix(a float64) matrix2x2 { 55 | return matrix2x2{ 56 | math.Cos(a), -math.Sin(a), 57 | math.Sin(a), math.Cos(a), 58 | } 59 | } 60 | 61 | func degToRad(a float64) float64 { 62 | return a * (math.Pi / 180.0) 63 | } 64 | 65 | func transformRect(m matrix2x2, r image.Rectangle) (output image.Rectangle) { 66 | ps := []image.Point{ 67 | {r.Min.X, r.Min.Y}, 68 | {r.Max.X, r.Min.Y}, 69 | {r.Min.X, r.Max.Y}, 70 | {r.Max.X, r.Max.Y}, 71 | } 72 | 73 | for i, p := range ps { 74 | sxf := float64(p.X) 75 | syf := float64(p.Y) 76 | dxi, dyi := translateCoordinatesWithMatrix(sxf, syf, m) 77 | dx, dy := int(math.Floor(dxi)), int(math.Floor(dyi)) 78 | 79 | if i == 0 { 80 | output = image.Rectangle{ 81 | Min: image.Point{dx + 0, dy + 0}, 82 | Max: image.Point{dx + 1, dy + 1}, 83 | } 84 | 85 | continue 86 | } 87 | 88 | if output.Min.X > dx { 89 | output.Min.X = dx 90 | } 91 | 92 | dx++ 93 | 94 | if output.Max.X < dx { 95 | output.Max.X = dx 96 | } 97 | 98 | if output.Min.Y > dy { 99 | output.Min.Y = dy 100 | } 101 | 102 | dy++ 103 | 104 | if output.Max.Y < dy { 105 | output.Max.Y = dy 106 | } 107 | } 108 | 109 | return output 110 | } 111 | 112 | func translateCoordinatesWithMatrix(x, y float64, b matrix2x2) (float64, float64) { 113 | return b[0]*x + b[1]*y, b[2]*x + b[3]*y 114 | } 115 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | // Options is the options struct used by most all render methods. 4 | type Options struct { 5 | Scale int 6 | Overlay bool 7 | Slim bool 8 | Square bool 9 | } 10 | -------------------------------------------------------------------------------- /parts.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import "image" 4 | 5 | var ( 6 | // HeadTop is the top side of the head 7 | HeadTop image.Rectangle = image.Rect(8, 0, 16, 8) 8 | // HeadBottom is the bottom side of the head 9 | HeadBottom image.Rectangle = image.Rect(16, 0, 24, 8) 10 | // HeadRight is the right side of the head 11 | HeadRight image.Rectangle = image.Rect(0, 8, 8, 16) 12 | // HeadFront is the front side of the head 13 | HeadFront image.Rectangle = image.Rect(8, 8, 16, 16) 14 | // HeadLeft is the left side of the head 15 | HeadLeft image.Rectangle = image.Rect(16, 8, 24, 16) 16 | // HeadBack is the back side of the head 17 | HeadBack image.Rectangle = image.Rect(24, 8, 32, 16) 18 | // HeadOverlayTop is the top side of the head overlay 19 | HeadOverlayTop image.Rectangle = image.Rect(40, 0, 48, 8) 20 | // HeadOverlayBottom is the bottom side of the head overlay 21 | HeadOverlayBottom image.Rectangle = image.Rect(48, 0, 56, 8) 22 | // HeadOverlayRight is the right side of the head overlay 23 | HeadOverlayRight image.Rectangle = image.Rect(32, 8, 40, 16) 24 | // HeadOverlayFront is the front side of the head overlay 25 | HeadOverlayFront image.Rectangle = image.Rect(40, 8, 48, 16) 26 | // HeadOverlayLeft is the left side of the head overlay 27 | HeadOverlayLeft image.Rectangle = image.Rect(48, 8, 56, 16) 28 | // HeadOverlayBack is the back side of the head overlay 29 | HeadOverlayBack image.Rectangle = image.Rect(56, 8, 64, 16) 30 | // RightLegTop is the top side of the right leg 31 | RightLegTop image.Rectangle = image.Rect(4, 16, 8, 20) 32 | // RightLegBottom is the bottom side of the right leg 33 | RightLegBottom image.Rectangle = image.Rect(8, 16, 12, 20) 34 | // RightLegRight is the right side of the right leg 35 | RightLegRight image.Rectangle = image.Rect(0, 20, 4, 32) 36 | // RightLegFront is the front side of the right leg 37 | RightLegFront image.Rectangle = image.Rect(4, 20, 8, 32) 38 | // RightLegLeft is the left side of the right leg 39 | RightLegLeft image.Rectangle = image.Rect(8, 20, 12, 32) 40 | // RightLegBack is the back side of the right leg 41 | RightLegBack image.Rectangle = image.Rect(12, 20, 16, 32) 42 | // TorsoTop is the top side of the torso 43 | TorsoTop image.Rectangle = image.Rect(20, 16, 28, 20) 44 | // TorsoBottom is the bottom side of the torso 45 | TorsoBottom image.Rectangle = image.Rect(28, 16, 36, 20) 46 | // TorsoRight is the right side of the torso 47 | TorsoRight image.Rectangle = image.Rect(16, 20, 20, 32) 48 | // TorsoFront is the front side of the torso 49 | TorsoFront image.Rectangle = image.Rect(20, 20, 28, 32) 50 | // TorsoLeft is the left side of the torso 51 | TorsoLeft image.Rectangle = image.Rect(28, 20, 32, 32) 52 | // TorsoBack is the back side of the torso 53 | TorsoBack image.Rectangle = image.Rect(32, 20, 40, 32) 54 | // RightArmTopRegular is the top side of the right arm for regular skin models 55 | RightArmTopRegular image.Rectangle = image.Rect(44, 16, 48, 20) 56 | // RightArmTopSlim is the top side of the right arm for slim skin models 57 | RightArmTopSlim image.Rectangle = image.Rect(44, 16, 47, 20) 58 | // RightArmBottomRegular is the bottom side of the right arm for regular skin models 59 | RightArmBottomRegular image.Rectangle = image.Rect(48, 16, 52, 20) 60 | // RightArmBottomSlim is the bottom side of the right arm for slim skin models 61 | RightArmBottomSlim image.Rectangle = image.Rect(47, 16, 50, 20) 62 | // RightArmRight is the right side of the right arm 63 | RightArmRight image.Rectangle = image.Rect(40, 20, 44, 32) 64 | // RightArmFrontRegular is the front side of the right arm for regular skin models 65 | RightArmFrontRegular image.Rectangle = image.Rect(44, 20, 48, 32) 66 | // RightArmFrontSlim is the front side of the right arm for slim skin models 67 | RightArmFrontSlim image.Rectangle = image.Rect(44, 20, 47, 32) 68 | // RightArmLeftRegular is the left side of the right arm for regular skin models 69 | RightArmLeftRegular image.Rectangle = image.Rect(48, 20, 52, 32) 70 | // RightArmLeftSlim is the left side of the right arm for slim skin models 71 | RightArmLeftSlim image.Rectangle = image.Rect(47, 20, 51, 32) 72 | // RightArmBackRegular is the back side of the right arm for regular skin models 73 | RightArmBackRegular image.Rectangle = image.Rect(52, 20, 56, 32) 74 | // RightArmBackSlim is the back side of the right arm for slim skin models 75 | RightArmBackSlim image.Rectangle = image.Rect(51, 20, 54, 32) 76 | // LeftLegTop is the top side of the left leg 77 | LeftLegTop image.Rectangle = image.Rect(20, 48, 24, 52) 78 | // LeftLegBottom is the bottom side of the left leg 79 | LeftLegBottom image.Rectangle = image.Rect(24, 48, 28, 52) 80 | // LeftLegRight is the right side of the left leg 81 | LeftLegRight image.Rectangle = image.Rect(16, 52, 20, 64) 82 | // LeftLegFront is the front side of the left leg 83 | LeftLegFront image.Rectangle = image.Rect(20, 52, 24, 64) 84 | // LeftLegLeft is the left side of the left leg 85 | LeftLegLeft image.Rectangle = image.Rect(24, 52, 28, 64) 86 | // LeftLegBack is the back side of the left leg 87 | LeftLegBack image.Rectangle = image.Rect(28, 52, 32, 64) 88 | // LeftArmTopRegular is the top side of the left arm for regular skin models 89 | LeftArmTopRegular image.Rectangle = image.Rect(36, 48, 40, 52) 90 | // LeftArmTopSlim is the top side of the left arm for slim skin models 91 | LeftArmTopSlim image.Rectangle = image.Rect(36, 48, 39, 52) 92 | // LeftArmBottomRegular is the bottom side of the left arm for regular skin models 93 | LeftArmBottomRegular image.Rectangle = image.Rect(40, 48, 44, 52) 94 | // LeftArmBottomSlim is the bottom side of the left arm for slim skin models 95 | LeftArmBottomSlim image.Rectangle = image.Rect(39, 48, 42, 52) 96 | // LeftArmRight is the right side of the left arm 97 | LeftArmRight image.Rectangle = image.Rect(32, 52, 36, 64) 98 | // LeftArmFrontRegular is the front side of the left arm for regular skin models 99 | LeftArmFrontRegular image.Rectangle = image.Rect(36, 52, 40, 64) 100 | // LeftArmFrontSlim is the front side of the left arm for slim skin models 101 | LeftArmFrontSlim image.Rectangle = image.Rect(36, 52, 39, 64) 102 | // LeftArmLeftRegular is the left side of the left arm for regular skin models 103 | LeftArmLeftRegular image.Rectangle = image.Rect(40, 52, 44, 64) 104 | // LeftArmLeftSlim is the left side of the left arm for slim skin models 105 | LeftArmLeftSlim image.Rectangle = image.Rect(39, 52, 43, 64) 106 | // LeftArmBackRegular is the back side of the left arm for regular skin models 107 | LeftArmBackRegular image.Rectangle = image.Rect(44, 52, 48, 64) 108 | // LeftArmBackSlim is the back side of the left arm for slim skin models 109 | LeftArmBackSlim image.Rectangle = image.Rect(43, 52, 46, 64) 110 | // RightLegOverlayTop is the top side of the right leg overlay 111 | RightLegOverlayTop image.Rectangle = image.Rect(4, 48, 8, 52) 112 | // RightLegOverlayBottom is the bottom side of the right leg overlay 113 | RightLegOverlayBottom image.Rectangle = image.Rect(8, 48, 12, 52) 114 | // RightLegOverlayRight is the right side of the right leg overlay 115 | RightLegOverlayRight image.Rectangle = image.Rect(0, 36, 4, 48) 116 | // RightLegOverlayFront is the front side of the right leg overlay 117 | RightLegOverlayFront image.Rectangle = image.Rect(4, 36, 8, 48) 118 | // RightLegOverlayLeft is the left side of the right leg overlay 119 | RightLegOverlayLeft image.Rectangle = image.Rect(8, 36, 12, 48) 120 | // RightLegOverlayBack is the back side of the right leg overlay 121 | RightLegOverlayBack image.Rectangle = image.Rect(12, 36, 16, 48) 122 | // TorsoOverlayTop is the top side of the torso overlay 123 | TorsoOverlayTop image.Rectangle = image.Rect(20, 48, 28, 52) 124 | // TorsoOverlayBottom is the bottom side of the torso overlay 125 | TorsoOverlayBottom image.Rectangle = image.Rect(28, 48, 36, 52) 126 | // TorsoOverlayRight is the right side of the torso overlay 127 | TorsoOverlayRight image.Rectangle = image.Rect(16, 36, 20, 48) 128 | // TorsoOverlayFront is the front side of the torso overlay 129 | TorsoOverlayFront image.Rectangle = image.Rect(20, 36, 28, 48) 130 | // TorsoOverlayLeft is the left side of the torso overlay 131 | TorsoOverlayLeft image.Rectangle = image.Rect(28, 36, 32, 48) 132 | // TorsoOverlayBack is the back side of the torso overlay 133 | TorsoOverlayBack image.Rectangle = image.Rect(32, 36, 40, 48) 134 | // RightArmOverlayTopRegular is the top side of the right arm overlay for regular skin models 135 | RightArmOverlayTopRegular image.Rectangle = image.Rect(44, 48, 48, 52) 136 | // RightArmOverlayTopSlim is the top side of the right arm overlay for slim skin models 137 | RightArmOverlayTopSlim image.Rectangle = image.Rect(44, 48, 47, 52) 138 | // RightArmOverlayBottomRegular is the bottom side of the right arm overlay for regular skin models 139 | RightArmOverlayBottomRegular image.Rectangle = image.Rect(48, 48, 52, 52) 140 | // RightArmOverlayBottomSlim is the bottom side of the right arm overlay for slim skin models 141 | RightArmOverlayBottomSlim image.Rectangle = image.Rect(47, 48, 50, 52) 142 | // RightArmOverlayRight is the right side of the right arm overlay 143 | RightArmOverlayRight image.Rectangle = image.Rect(40, 36, 44, 48) 144 | // RightArmOverlayFrontRegular is the front side of the right arm overlay for regular skin models 145 | RightArmOverlayFrontRegular image.Rectangle = image.Rect(44, 36, 48, 48) 146 | // RightArmOverlayFrontSlim is the front side of the right arm overlay for slim skin models 147 | RightArmOverlayFrontSlim image.Rectangle = image.Rect(44, 36, 47, 48) 148 | // RightArmOverlayLeftRegular is the left side of the right arm overlay for regular skin models 149 | RightArmOverlayLeftRegular image.Rectangle = image.Rect(48, 36, 52, 48) 150 | // RightArmOverlayLeftSlim is the left side of the right arm overlay for slim skin models 151 | RightArmOverlayLeftSlim image.Rectangle = image.Rect(47, 36, 51, 48) 152 | // RightArmOverlayBackRegular is the back side of the right arm overlay for regular skin models 153 | RightArmOverlayBackRegular image.Rectangle = image.Rect(52, 36, 56, 48) 154 | // RightArmOverlayBackSlim is the back side of the right arm overlay for slim skin models 155 | RightArmOverlayBackSlim image.Rectangle = image.Rect(51, 36, 54, 48) 156 | // LeftLegOverlayTop is the top side of the left leg overlay 157 | LeftLegOverlayTop image.Rectangle = image.Rect(4, 48, 8, 52) 158 | // LeftLegOverlayBottom is the bottom side of the left leg overlay 159 | LeftLegOverlayBottom image.Rectangle = image.Rect(8, 48, 12, 52) 160 | // LeftLegOverlayRight is the right side of the left leg overlay 161 | LeftLegOverlayRight image.Rectangle = image.Rect(0, 52, 4, 64) 162 | // LeftLegOverlayFront is the front side of the left leg overlay 163 | LeftLegOverlayFront image.Rectangle = image.Rect(4, 52, 8, 64) 164 | // LeftLegOverlayLeft is the left side of the left leg overlay 165 | LeftLegOverlayLeft image.Rectangle = image.Rect(8, 52, 12, 64) 166 | // LeftLegOverlayBack is the back side of the left leg overlay 167 | LeftLegOverlayBack image.Rectangle = image.Rect(12, 52, 16, 64) 168 | // LeftArmOverlayTopRegular is the top side of the left arm overlay for regular skin models 169 | LeftArmOverlayTopRegular image.Rectangle = image.Rect(52, 48, 56, 52) 170 | // LeftArmOverlayTopSlim is the top side of the left arm overlay for slim skin models 171 | LeftArmOverlayTopSlim image.Rectangle = image.Rect(52, 48, 55, 52) 172 | // LeftArmOverlayBottomRegular is the bottom side of the left arm overlay for regular skin models 173 | LeftArmOverlayBottomRegular image.Rectangle = image.Rect(56, 48, 60, 52) 174 | // LeftArmOverlayBottomSlim is the bottom side of the left arm overlay for slim skin models 175 | LeftArmOverlayBottomSlim image.Rectangle = image.Rect(55, 48, 58, 52) 176 | // LeftArmOverlayRight is the right side of the left arm overlay 177 | LeftArmOverlayRight image.Rectangle = image.Rect(48, 52, 52, 64) 178 | // LeftArmOverlayFrontRegular is the front side of the left arm overlay for regular skin models 179 | LeftArmOverlayFrontRegular image.Rectangle = image.Rect(52, 52, 56, 64) 180 | // LeftArmOverlayFrontSlim is the front side of the left arm overlay for slim skin models 181 | LeftArmOverlayFrontSlim image.Rectangle = image.Rect(52, 52, 55, 64) 182 | // LeftArmOverlayLeftRegular is the left side of the left arm overlay for regular skin models 183 | LeftArmOverlayLeftRegular image.Rectangle = image.Rect(56, 52, 60, 64) 184 | // LeftArmOverlayLeftSlim is the left side of the left arm overlay for slim skin models 185 | LeftArmOverlayLeftSlim image.Rectangle = image.Rect(55, 52, 59, 64) 186 | // LeftArmOverlayBackRegular is the back side of the left arm overlay for regular skin models 187 | LeftArmOverlayBackRegular image.Rectangle = image.Rect(60, 52, 64, 64) 188 | // LeftArmOverlayBackSlim is the back side of the left arm overlay for slim skin models 189 | LeftArmOverlayBackSlim image.Rectangle = image.Rect(59, 52, 62, 64) 190 | // PartsList is a list of all part coordinates of a Minecraft skin 191 | PartsList map[string]image.Rectangle = map[string]image.Rectangle{ 192 | "HeadTop": HeadTop, 193 | "HeadBottom": HeadBottom, 194 | "HeadRight": HeadRight, 195 | "HeadFront": HeadFront, 196 | "HeadLeft": HeadLeft, 197 | "HeadBack": HeadBack, 198 | "HeadOverlayTop": HeadOverlayTop, 199 | "HeadOverlayBottom": HeadOverlayBottom, 200 | "HeadOverlayRight": HeadOverlayRight, 201 | "HeadOverlayFront": HeadOverlayFront, 202 | "HeadOverlayLeft": HeadOverlayLeft, 203 | "HeadOverlayBack": HeadOverlayBack, 204 | "RightLegTop": RightLegTop, 205 | "RightLegBottom": RightLegBottom, 206 | "RightLegRight": RightLegRight, 207 | "RightLegFront": RightLegFront, 208 | "RightLegLeft": RightLegLeft, 209 | "RightLegBack": RightLegBack, 210 | "TorsoTop": TorsoTop, 211 | "TorsoBottom": TorsoBottom, 212 | "TorsoRight": TorsoRight, 213 | "TorsoFront": TorsoFront, 214 | "TorsoLeft": TorsoLeft, 215 | "TorsoBack": TorsoBack, 216 | "RightArmTopRegular": RightArmTopRegular, 217 | "RightArmTopSlim": RightArmTopSlim, 218 | "RightArmBottomRegular": RightArmBottomRegular, 219 | "RightArmBottomSlim": RightArmBottomSlim, 220 | "RightArmRight": RightArmRight, 221 | "RightArmFrontRegular": RightArmFrontRegular, 222 | "RightArmFrontSlim": RightArmFrontSlim, 223 | "RightArmLeftRegular": RightArmLeftRegular, 224 | "RightArmLeftSlim": RightArmLeftSlim, 225 | "RightArmBackRegular": RightArmBackRegular, 226 | "RightArmBackSlim": RightArmBackSlim, 227 | "LeftLegTop": LeftLegTop, 228 | "LeftLegBottom": LeftLegBottom, 229 | "LeftLegRight": LeftLegRight, 230 | "LeftLegFront": LeftLegFront, 231 | "LeftLegLeft": LeftLegLeft, 232 | "LeftLegBack": LeftLegBack, 233 | "LeftArmTopRegular": LeftArmTopRegular, 234 | "LeftArmTopSlim": LeftArmTopSlim, 235 | "LeftArmBottomRegular": LeftArmBottomRegular, 236 | "LeftArmBottomSlim": LeftArmBottomSlim, 237 | "LeftArmRight": LeftArmRight, 238 | "LeftArmFrontRegular": LeftArmFrontRegular, 239 | "LeftArmFrontSlim": LeftArmFrontSlim, 240 | "LeftArmLeftRegular": LeftArmLeftRegular, 241 | "LeftArmLeftSlim": LeftArmLeftSlim, 242 | "LeftArmBackRegular": LeftArmBackRegular, 243 | "LeftArmBackSlim": LeftArmBackSlim, 244 | "RightLegOverlayTop": RightLegOverlayTop, 245 | "RightLegOverlayBottom": RightLegOverlayBottom, 246 | "RightLegOverlayRight": RightLegOverlayRight, 247 | "RightLegOverlayFront": RightLegOverlayFront, 248 | "RightLegOverlayLeft": RightLegOverlayLeft, 249 | "RightLegOverlayBack": RightLegOverlayBack, 250 | "TorsoOverlayTop": TorsoOverlayTop, 251 | "TorsoOverlayBottom": TorsoOverlayBottom, 252 | "TorsoOverlayRight": TorsoOverlayRight, 253 | "TorsoOverlayFront": TorsoOverlayFront, 254 | "TorsoOverlayLeft": TorsoOverlayLeft, 255 | "TorsoOverlayBack": TorsoOverlayBack, 256 | "RightArmOverlayTopRegular": RightArmOverlayTopRegular, 257 | "RightArmOverlayTopSlim": RightArmOverlayTopSlim, 258 | "RightArmOverlayBottomRegular": RightArmOverlayBottomRegular, 259 | "RightArmOverlayBottomSlim": RightArmOverlayBottomSlim, 260 | "RightArmOverlayRight": RightArmOverlayRight, 261 | "RightArmOverlayFrontRegular": RightArmOverlayFrontRegular, 262 | "RightArmOverlayFrontSlim": RightArmOverlayFrontSlim, 263 | "RightArmOverlayLeftRegular": RightArmOverlayLeftRegular, 264 | "RightArmOverlayLeftSlim": RightArmOverlayLeftSlim, 265 | "RightArmOverlayBackRegular": RightArmOverlayBackRegular, 266 | "RightArmOverlayBackSlim": RightArmOverlayBackSlim, 267 | "LeftLegOverlayTop": LeftLegOverlayTop, 268 | "LeftLegOverlayBottom": LeftLegOverlayBottom, 269 | "LeftLegOverlayRight": LeftLegOverlayRight, 270 | "LeftLegOverlayFront": LeftLegOverlayFront, 271 | "LeftLegOverlayLeft": LeftLegOverlayLeft, 272 | "LeftLegOverlayBack": LeftLegOverlayBack, 273 | "LeftArmOverlayTopRegular": LeftArmOverlayTopRegular, 274 | "LeftArmOverlayTopSlim": LeftArmOverlayTopSlim, 275 | "LeftArmOverlayBottomRegular": LeftArmOverlayBottomRegular, 276 | "LeftArmOverlayBottomSlim": LeftArmOverlayBottomSlim, 277 | "LeftArmOverlayRight": LeftArmOverlayRight, 278 | "LeftArmOverlayFrontRegular": LeftArmOverlayFrontRegular, 279 | "LeftArmOverlayFrontSlim": LeftArmOverlayFrontSlim, 280 | "LeftArmOverlayLeftRegular": LeftArmOverlayLeftRegular, 281 | "LeftArmOverlayLeftSlim": LeftArmOverlayLeftSlim, 282 | "LeftArmOverlayBackRegular": LeftArmOverlayBackRegular, 283 | "LeftArmOverlayBackSlim": LeftArmOverlayBackSlim, 284 | } 285 | ) 286 | 287 | // GetRightArmTop returns the top of a right arm based on if the skin is slim or not 288 | func GetRightArmTop(slim bool) image.Rectangle { 289 | if slim { 290 | return RightArmTopSlim 291 | } 292 | 293 | return RightArmTopRegular 294 | } 295 | 296 | // GetRightArmBottom returns the bottom of a right arm based on if the skin is slim or not 297 | func GetRightArmBottom(slim bool) image.Rectangle { 298 | if slim { 299 | return RightArmBottomSlim 300 | } 301 | 302 | return RightArmBottomRegular 303 | } 304 | 305 | // GetRightArmFront returns the front of a right arm based on if the skin is slim or not 306 | func GetRightArmFront(slim bool) image.Rectangle { 307 | if slim { 308 | return RightArmFrontSlim 309 | } 310 | 311 | return RightArmFrontRegular 312 | } 313 | 314 | // GetRightArmLeft returns the left of a right arm based on if the skin is slim or not 315 | func GetRightArmLeft(slim bool) image.Rectangle { 316 | if slim { 317 | return RightArmLeftSlim 318 | } 319 | 320 | return RightArmLeftRegular 321 | } 322 | 323 | // GetRightArmBack returns the back of a right arm based on if the skin is slim or not 324 | func GetRightArmBack(slim bool) image.Rectangle { 325 | if slim { 326 | return RightArmBackSlim 327 | } 328 | 329 | return RightArmBackRegular 330 | } 331 | 332 | // GetLeftArmTop returns the top of a left arm based on if the skin is slim or not 333 | func GetLeftArmTop(slim bool) image.Rectangle { 334 | if slim { 335 | return LeftArmTopSlim 336 | } 337 | 338 | return LeftArmTopRegular 339 | } 340 | 341 | // GetLeftArmBottom returns the bottom of a left arm based on if the skin is slim or not 342 | func GetLeftArmBottom(slim bool) image.Rectangle { 343 | if slim { 344 | return LeftArmBottomSlim 345 | } 346 | 347 | return LeftArmBottomRegular 348 | } 349 | 350 | // GetLeftArmFront returns the front of a left arm based on if the skin is slim or not 351 | func GetLeftArmFront(slim bool) image.Rectangle { 352 | if slim { 353 | return LeftArmFrontSlim 354 | } 355 | 356 | return LeftArmFrontRegular 357 | } 358 | 359 | // GetLeftArmLeft returns the left of a left arm based on if the skin is slim or not 360 | func GetLeftArmLeft(slim bool) image.Rectangle { 361 | if slim { 362 | return LeftArmLeftSlim 363 | } 364 | 365 | return LeftArmLeftRegular 366 | } 367 | 368 | // GetLeftArmBack returns the back of a left arm based on if the skin is slim or not 369 | func GetLeftArmBack(slim bool) image.Rectangle { 370 | if slim { 371 | return LeftArmBackSlim 372 | } 373 | 374 | return LeftArmBackRegular 375 | } 376 | 377 | // GetRightArmOverlayTop returns the top of a right arm overlay based on if the skin is slim or not 378 | func GetRightArmOverlayTop(slim bool) image.Rectangle { 379 | if slim { 380 | return RightArmOverlayTopSlim 381 | } 382 | 383 | return RightArmOverlayTopRegular 384 | } 385 | 386 | // GetRightArmOverlayBottom returns the bottom of a right arm overlay based on if the skin is slim or not 387 | func GetRightArmOverlayBottom(slim bool) image.Rectangle { 388 | if slim { 389 | return RightArmOverlayBottomSlim 390 | } 391 | 392 | return RightArmOverlayBottomRegular 393 | } 394 | 395 | // GetRightArmOverlayFront returns the front of a right arm overlay based on if the skin is slim or not 396 | func GetRightArmOverlayFront(slim bool) image.Rectangle { 397 | if slim { 398 | return RightArmOverlayFrontSlim 399 | } 400 | 401 | return RightArmOverlayFrontRegular 402 | } 403 | 404 | // GetRightArmOverlayLeft returns the left of a right arm overlay based on if the skin is slim or not 405 | func GetRightArmOverlayLeft(slim bool) image.Rectangle { 406 | if slim { 407 | return RightArmOverlayLeftSlim 408 | } 409 | 410 | return RightArmOverlayLeftRegular 411 | } 412 | 413 | // GetRightArmOverlayBack returns the back of a right arm overlay based on if the skin is slim or not 414 | func GetRightArmOverlayBack(slim bool) image.Rectangle { 415 | if slim { 416 | return RightArmOverlayBackSlim 417 | } 418 | 419 | return RightArmOverlayBackRegular 420 | } 421 | 422 | // GetLeftArmOverlayTop returns the top of a left arm overlay based on if the skin is slim or not 423 | func GetLeftArmOverlayTop(slim bool) image.Rectangle { 424 | if slim { 425 | return LeftArmOverlayTopSlim 426 | } 427 | 428 | return LeftArmOverlayTopRegular 429 | } 430 | 431 | // GetLeftArmOverlayBottom returns the bottom of a left arm overlay based on if the skin is slim or not 432 | func GetLeftArmOverlayBottom(slim bool) image.Rectangle { 433 | if slim { 434 | return LeftArmOverlayBottomSlim 435 | } 436 | 437 | return LeftArmOverlayBottomRegular 438 | } 439 | 440 | // GetLeftArmOverlayFront returns the front of a left arm overlay based on if the skin is slim or not 441 | func GetLeftArmOverlayFront(slim bool) image.Rectangle { 442 | if slim { 443 | return LeftArmOverlayFrontSlim 444 | } 445 | 446 | return LeftArmOverlayFrontRegular 447 | } 448 | 449 | // GetLeftArmOverlayLeft returns the left of a left arm overlay based on if the skin is slim or not 450 | func GetLeftArmOverlayLeft(slim bool) image.Rectangle { 451 | if slim { 452 | return LeftArmOverlayLeftSlim 453 | } 454 | 455 | return LeftArmOverlayLeftRegular 456 | } 457 | 458 | // GetLeftArmOverlayBack returns the back of a left arm overlay based on if the skin is slim or not 459 | func GetLeftArmOverlayBack(slim bool) image.Rectangle { 460 | if slim { 461 | return LeftArmOverlayBackSlim 462 | } 463 | 464 | return LeftArmOverlayBackRegular 465 | } 466 | -------------------------------------------------------------------------------- /parts_test.go: -------------------------------------------------------------------------------- 1 | package skin_test 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/mineatar-io/skin-render" 8 | ) 9 | 10 | var ( 11 | headComponents []image.Rectangle = []image.Rectangle{ 12 | skin.HeadTop, 13 | skin.HeadBottom, 14 | skin.HeadRight, 15 | skin.HeadFront, 16 | skin.HeadLeft, 17 | skin.HeadBack, 18 | skin.HeadOverlayTop, 19 | skin.HeadOverlayBottom, 20 | skin.HeadOverlayRight, 21 | skin.HeadOverlayFront, 22 | skin.HeadOverlayLeft, 23 | skin.HeadOverlayBack, 24 | } 25 | armRegularSideComponents []image.Rectangle = []image.Rectangle{ 26 | skin.RightArmRight, 27 | skin.RightArmFrontRegular, 28 | skin.RightArmLeftRegular, 29 | skin.RightArmLeftSlim, 30 | skin.RightArmBackRegular, 31 | skin.LeftArmFrontRegular, 32 | skin.LeftArmLeftRegular, 33 | skin.LeftArmLeftSlim, 34 | skin.LeftArmBackRegular, 35 | skin.RightArmOverlayRight, 36 | skin.RightArmOverlayFrontRegular, 37 | skin.RightArmOverlayLeftRegular, 38 | skin.RightArmOverlayLeftSlim, 39 | skin.RightArmOverlayBackRegular, 40 | skin.LeftArmOverlayRight, 41 | skin.LeftArmOverlayFrontRegular, 42 | skin.LeftArmOverlayLeftRegular, 43 | skin.LeftArmOverlayLeftSlim, 44 | skin.LeftArmOverlayBackRegular, 45 | } 46 | armRegularTopComponents []image.Rectangle = []image.Rectangle{ 47 | skin.RightArmTopRegular, 48 | skin.RightArmBottomRegular, 49 | skin.LeftArmTopRegular, 50 | skin.LeftArmBottomRegular, 51 | skin.RightArmOverlayTopRegular, 52 | skin.RightArmOverlayBottomRegular, 53 | skin.LeftArmOverlayTopRegular, 54 | skin.LeftArmOverlayBottomRegular, 55 | } 56 | armSlimSideComponents []image.Rectangle = []image.Rectangle{ 57 | skin.RightArmFrontSlim, 58 | skin.RightArmBackSlim, 59 | skin.LeftArmFrontSlim, 60 | skin.LeftArmBackSlim, 61 | skin.RightArmOverlayFrontSlim, 62 | skin.RightArmOverlayBackSlim, 63 | skin.LeftArmOverlayFrontSlim, 64 | skin.LeftArmOverlayBackSlim, 65 | } 66 | armSlimTopComponents []image.Rectangle = []image.Rectangle{ 67 | skin.RightArmTopSlim, 68 | skin.RightArmBottomSlim, 69 | skin.LeftArmTopSlim, 70 | skin.LeftArmBottomSlim, 71 | skin.RightArmOverlayTopSlim, 72 | skin.RightArmOverlayBottomSlim, 73 | skin.LeftArmOverlayTopSlim, 74 | skin.LeftArmOverlayBottomSlim, 75 | } 76 | legSideComponents []image.Rectangle = []image.Rectangle{ 77 | skin.LeftLegRight, 78 | skin.LeftLegFront, 79 | skin.LeftLegLeft, 80 | skin.LeftLegBack, 81 | skin.LeftArmRight, 82 | skin.RightLegRight, 83 | skin.RightLegFront, 84 | skin.RightLegLeft, 85 | skin.RightLegBack, 86 | skin.LeftLegOverlayRight, 87 | skin.LeftLegOverlayFront, 88 | skin.LeftLegOverlayLeft, 89 | skin.LeftLegOverlayBack, 90 | skin.RightLegOverlayRight, 91 | skin.RightLegOverlayFront, 92 | skin.RightLegOverlayLeft, 93 | skin.RightLegOverlayBack, 94 | } 95 | legTopComponents []image.Rectangle = []image.Rectangle{ 96 | skin.RightLegTop, 97 | skin.RightLegBottom, 98 | skin.LeftLegTop, 99 | skin.LeftLegBottom, 100 | skin.RightLegOverlayTop, 101 | skin.RightLegOverlayBottom, 102 | skin.LeftLegOverlayTop, 103 | skin.LeftLegOverlayBottom, 104 | } 105 | torsoFrontComponents []image.Rectangle = []image.Rectangle{ 106 | skin.TorsoFront, 107 | skin.TorsoBack, 108 | skin.TorsoOverlayFront, 109 | skin.TorsoOverlayBack, 110 | } 111 | torsoSideComponents []image.Rectangle = []image.Rectangle{ 112 | skin.TorsoRight, 113 | skin.TorsoLeft, 114 | skin.TorsoOverlayRight, 115 | skin.TorsoOverlayLeft, 116 | } 117 | torsoTopComponents []image.Rectangle = []image.Rectangle{ 118 | skin.TorsoTop, 119 | skin.TorsoBottom, 120 | skin.TorsoOverlayTop, 121 | skin.TorsoOverlayBottom, 122 | } 123 | allComponents [][]image.Rectangle = [][]image.Rectangle{ 124 | headComponents, 125 | armRegularSideComponents, 126 | armRegularTopComponents, 127 | armSlimSideComponents, 128 | armSlimTopComponents, 129 | legSideComponents, 130 | torsoFrontComponents, 131 | torsoSideComponents, 132 | torsoTopComponents, 133 | } 134 | ) 135 | 136 | func TestComponents(t *testing.T) { 137 | for kg, group := range allComponents { 138 | for kc, c := range group { 139 | if c.Max.X > c.Min.X && c.Max.Y > c.Min.Y { 140 | continue 141 | } 142 | 143 | t.Fatalf("group %d component %d has invalid dimensions: %s", kg, kc, c) 144 | } 145 | } 146 | } 147 | 148 | func TestHeadComponents(t *testing.T) { 149 | for k, c := range headComponents { 150 | if c.Dx() == 8 && c.Dy() == 8 { 151 | continue 152 | } 153 | 154 | t.Fatalf("head component %d has invalid dimensions: expected=(8,8) received=%s", k, c.Size()) 155 | } 156 | } 157 | 158 | func TestRegularArmSideComponents(t *testing.T) { 159 | for k, c := range armRegularSideComponents { 160 | if c.Dx() == 4 && c.Dy() == 12 { 161 | continue 162 | } 163 | 164 | t.Fatalf("regular arm side component %d has invalid dimensions: expected=(4,12) received=%s", k, c.Size()) 165 | } 166 | } 167 | 168 | func TestRegularArmTopComponents(t *testing.T) { 169 | for k, c := range armRegularTopComponents { 170 | if c.Dx() == 4 && c.Dy() == 4 { 171 | continue 172 | } 173 | 174 | t.Fatalf("regular arm top component %d has invalid dimensions: expected=(4,4) received=%s", k, c.Size()) 175 | } 176 | } 177 | 178 | func TestSlimArmSideComponents(t *testing.T) { 179 | for k, c := range armSlimSideComponents { 180 | if c.Dx() == 3 && c.Dy() == 12 { 181 | continue 182 | } 183 | 184 | t.Fatalf("slim arm side component %d has invalid dimensions: expected=(3,12) received=%s", k, c.Size()) 185 | } 186 | } 187 | 188 | func TestSlimArmTopComponents(t *testing.T) { 189 | for k, c := range armSlimTopComponents { 190 | if c.Dx() == 3 && c.Dy() == 4 { 191 | continue 192 | } 193 | 194 | t.Fatalf("slim arm top component %d has invalid dimensions: expected=(3,4) received=%s", k, c.Size()) 195 | } 196 | } 197 | 198 | func TestLegSideComponents(t *testing.T) { 199 | for k, c := range legSideComponents { 200 | if c.Dx() == 4 && c.Dy() == 12 { 201 | continue 202 | } 203 | 204 | t.Fatalf("leg side component %d has invalid dimensions: expected=(4,12) received=%s", k, c.Size()) 205 | } 206 | } 207 | 208 | func TestLegTopComponents(t *testing.T) { 209 | for k, c := range legTopComponents { 210 | if c.Dx() == 4 && c.Dy() == 4 { 211 | continue 212 | } 213 | 214 | t.Fatalf("leg top component %d has invalid dimensions: expected=(4,4) received=%s", k, c.Size()) 215 | } 216 | } 217 | 218 | func TestTorsoFrontComponents(t *testing.T) { 219 | for k, c := range torsoFrontComponents { 220 | if c.Dx() == 8 && c.Dy() == 12 { 221 | continue 222 | } 223 | 224 | t.Fatalf("torso front component %d has invalid dimensions: expected=(8,12) received=%s", k, c.Size()) 225 | } 226 | } 227 | 228 | func TestTorsoSideComponents(t *testing.T) { 229 | for k, c := range torsoSideComponents { 230 | if c.Dx() == 4 && c.Dy() == 12 { 231 | continue 232 | } 233 | 234 | t.Fatalf("torso side component %d has invalid dimensions: expected=(4,12) received=%s", k, c.Size()) 235 | } 236 | } 237 | 238 | func TestTorsoTopComponents(t *testing.T) { 239 | for k, c := range torsoTopComponents { 240 | if c.Dx() == 8 && c.Dy() == 4 { 241 | continue 242 | } 243 | 244 | t.Fatalf("torso top component %d has invalid dimensions: expected=(8,4) received=%s", k, c.Size()) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /rightbody.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | // RenderRightBody renders a 2-dimensional image of the right side of a Minecraft player's skin. 8 | func RenderRightBody(img *image.NRGBA, opts Options) *image.NRGBA { 9 | if err := validateSkin(img); err != nil { 10 | panic(err) 11 | } 12 | 13 | var ( 14 | skin *image.NRGBA = convertToNRGBA(img) 15 | output *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, 16, 32)) 16 | rightHead *image.NRGBA = removeTransparency(extract(skin, HeadRight)) 17 | rightRightArm *image.NRGBA = removeTransparency(extract(skin, RightArmRight)) 18 | rightRightLeg *image.NRGBA = removeTransparency(extract(skin, RightLegRight)) 19 | ) 20 | 21 | if opts.Overlay { 22 | overlaySkin := fixTransparency(skin) 23 | 24 | composite(rightHead, extract(overlaySkin, HeadOverlayRight), 0, 0) 25 | 26 | if !IsOldSkin(skin) { 27 | composite(rightRightArm, extract(overlaySkin, RightArmOverlayRight), 0, 0) 28 | composite(rightRightLeg, extract(overlaySkin, RightLegOverlayRight), 0, 0) 29 | } 30 | } 31 | 32 | // Right Head 33 | composite(output, rightHead, 4, 0) 34 | 35 | // Right Arm 36 | composite(output, rightRightArm, 6, 8) 37 | 38 | // Right Leg 39 | composite(output, rightRightLeg, 6, 20) 40 | 41 | if opts.Square { 42 | output = squareAndCenter(output) 43 | } 44 | 45 | return scale(output, opts.Scale) 46 | } 47 | -------------------------------------------------------------------------------- /rightbody_test.go: -------------------------------------------------------------------------------- 1 | package skin_test 2 | 3 | import ( 4 | "fmt" 5 | "image/png" 6 | "os" 7 | "testing" 8 | 9 | "github.com/mineatar-io/skin-render" 10 | ) 11 | 12 | func TestRightBodySteve(t *testing.T) { 13 | rawSkin := skin.GetDefaultSkin(false) 14 | 15 | for i := 0; i <= 8; i++ { 16 | scale := 1 << i 17 | 18 | output := skin.RenderRightBody(rawSkin, skin.Options{ 19 | Scale: scale, 20 | Overlay: true, 21 | Slim: false, 22 | Square: false, 23 | }) 24 | 25 | if output.Bounds().Dx() < 1 { 26 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 27 | } 28 | 29 | if output.Bounds().Dy() < 1 { 30 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 31 | } 32 | 33 | if writeRenders { 34 | f, err := os.OpenFile(fmt.Sprintf("rightbody_steve_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 35 | 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if err = png.Encode(f, output); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | if err = f.Close(); err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkRightBodySteve(b *testing.B) { 52 | rawSkin := skin.GetDefaultSkin(false) 53 | 54 | for n := 0; n <= b.N; n++ { 55 | skin.RenderRightBody(rawSkin, skin.Options{ 56 | Scale: defaultBenchmarkRenderScale, 57 | Overlay: true, 58 | Slim: false, 59 | Square: false, 60 | }) 61 | } 62 | } 63 | 64 | func TestRightBodyAlex(t *testing.T) { 65 | rawSkin := skin.GetDefaultSkin(true) 66 | 67 | for i := 0; i <= 8; i++ { 68 | scale := 1 << i 69 | 70 | output := skin.RenderRightBody(rawSkin, skin.Options{ 71 | Scale: scale, 72 | Overlay: true, 73 | Slim: true, 74 | Square: false, 75 | }) 76 | 77 | if output.Bounds().Dx() < 1 { 78 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 79 | } 80 | 81 | if output.Bounds().Dy() < 1 { 82 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 83 | } 84 | 85 | if writeRenders { 86 | f, err := os.OpenFile(fmt.Sprintf("rightbody_alex_test_%d.png", scale), os.O_CREATE|os.O_RDWR, 0777) 87 | 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if err = png.Encode(f, output); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if err = f.Close(); err != nil { 97 | t.Fatal(err) 98 | } 99 | } 100 | } 101 | } 102 | 103 | func BenchmarkRightBodyAlex(b *testing.B) { 104 | rawSkin := skin.GetDefaultSkin(true) 105 | 106 | for n := 0; n <= b.N; n++ { 107 | skin.RenderRightBody(rawSkin, skin.Options{ 108 | Scale: defaultBenchmarkRenderScale, 109 | Overlay: true, 110 | Slim: true, 111 | Square: false, 112 | }) 113 | } 114 | } 115 | 116 | func TestRightBodySteveSquare(t *testing.T) { 117 | rawSkin := skin.GetDefaultSkin(false) 118 | 119 | for i := 0; i <= 8; i++ { 120 | scale := 1 << i 121 | 122 | output := skin.RenderRightBody(rawSkin, skin.Options{ 123 | Scale: scale, 124 | Overlay: true, 125 | Slim: false, 126 | Square: true, 127 | }) 128 | 129 | if output.Bounds().Dx() < 1 { 130 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 131 | } 132 | 133 | if output.Bounds().Dy() < 1 { 134 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 135 | } 136 | 137 | if output.Bounds().Size().X != output.Bounds().Size().Y { 138 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 139 | } 140 | 141 | if writeRenders { 142 | f, err := os.OpenFile(fmt.Sprintf("rightbody_steve_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 143 | 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | if err = png.Encode(f, output); err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | if err = f.Close(); err != nil { 153 | t.Fatal(err) 154 | } 155 | } 156 | } 157 | } 158 | 159 | func BenchmarkRightBodySteveSquare(b *testing.B) { 160 | rawSkin := skin.GetDefaultSkin(false) 161 | 162 | for n := 0; n <= b.N; n++ { 163 | skin.RenderRightBody(rawSkin, skin.Options{ 164 | Scale: defaultBenchmarkRenderScale, 165 | Overlay: true, 166 | Slim: false, 167 | Square: true, 168 | }) 169 | } 170 | } 171 | 172 | func TestRightBodyAlexSquare(t *testing.T) { 173 | rawSkin := skin.GetDefaultSkin(true) 174 | 175 | for i := 0; i <= 8; i++ { 176 | scale := 1 << i 177 | 178 | output := skin.RenderRightBody(rawSkin, skin.Options{ 179 | Scale: scale, 180 | Overlay: true, 181 | Slim: true, 182 | Square: true, 183 | }) 184 | 185 | if output.Bounds().Dx() < 1 { 186 | t.Fatalf("result image width is %d pixels\n", output.Bounds().Dx()) 187 | } 188 | 189 | if output.Bounds().Dy() < 1 { 190 | t.Fatalf("result image height is %d pixels\n", output.Bounds().Dy()) 191 | } 192 | 193 | if output.Bounds().Size().X != output.Bounds().Size().Y { 194 | t.Fatalf("result image is not square (%s)\n", output.Bounds().Size()) 195 | } 196 | 197 | if writeRenders { 198 | f, err := os.OpenFile(fmt.Sprintf("rightbody_alex_test_%d_square.png", scale), os.O_CREATE|os.O_RDWR, 0777) 199 | 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | 204 | if err = png.Encode(f, output); err != nil { 205 | t.Fatal(err) 206 | } 207 | 208 | if err = f.Close(); err != nil { 209 | t.Fatal(err) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func BenchmarkRightBodyAlexSquare(b *testing.B) { 216 | rawSkin := skin.GetDefaultSkin(true) 217 | 218 | for n := 0; n <= b.N; n++ { 219 | skin.RenderRightBody(rawSkin, skin.Options{ 220 | Scale: defaultBenchmarkRenderScale, 221 | Overlay: true, 222 | Slim: true, 223 | Square: true, 224 | }) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /steve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mineatar-io/skin-render/16fa0fda282b061bad949032d6871167c9a1e8b1/steve.png -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package skin 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | 8 | // Used to embed the default skin images as a variable 9 | _ "embed" 10 | "image" 11 | "image/draw" 12 | "image/png" 13 | "strings" 14 | ) 15 | 16 | var ( 17 | //go:embed steve.png 18 | rawSteveSkinData []byte 19 | //go:embed alex.png 20 | rawAlexSkinData []byte 21 | steveSkin *image.NRGBA = nil 22 | alexSkin *image.NRGBA = nil 23 | zeroPoint image.Point = image.Point{} 24 | ) 25 | 26 | func init() { 27 | { 28 | rawSteveSkin, err := png.Decode(bytes.NewReader(rawSteveSkinData)) 29 | 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | steveSkin = image.NewNRGBA(rawSteveSkin.Bounds()) 35 | draw.Draw(steveSkin, rawSteveSkin.Bounds(), rawSteveSkin, image.Pt(0, 0), draw.Src) 36 | } 37 | 38 | { 39 | rawAlexSkin, err := png.Decode(bytes.NewReader(rawAlexSkinData)) 40 | 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | alexSkin = image.NewNRGBA(rawAlexSkin.Bounds()) 46 | draw.Draw(alexSkin, rawAlexSkin.Bounds(), rawAlexSkin, image.Pt(0, 0), draw.Src) 47 | } 48 | } 49 | 50 | // IsOldSkin returns a boolean which will be true if the skin is a legacy skin, which contains missing information about the skin overlay except for the head. 51 | func IsOldSkin(img image.Image) bool { 52 | return img.Bounds().Dy() < 64 53 | } 54 | 55 | // IsSlimFromUUID returns whether the skin is a slim variant from the UUID. 56 | // Credit: https://github.com/LapisBlue/Lapitar/blob/55ede80ce4ebb5ecc2b968164afb40f61b4cc509/mc/uuid.go#L34 57 | func IsSlimFromUUID(uuid string) bool { 58 | uuid = strings.ReplaceAll(uuid, "-", "") 59 | 60 | return (isEven(uuid[7]) != isEven(uuid[23])) != (isEven(uuid[15]) != isEven(uuid[31])) 61 | } 62 | 63 | // GetDefaultSkin returns the default skin for either a regular or slim variant of a Minecraft skin. 64 | func GetDefaultSkin(slim bool) *image.NRGBA { 65 | if slim { 66 | return alexSkin 67 | } 68 | 69 | return steveSkin 70 | } 71 | 72 | func validateSkin(img image.Image) error { 73 | if img.Bounds().Dx() == 64 && (img.Bounds().Dy() == 32 || img.Bounds().Dy() == 64) { 74 | return nil 75 | } 76 | 77 | return fmt.Errorf("invalid skin dimensions (received=%dx%d, expected=64x32 or 64x64)", img.Bounds().Dx(), img.Bounds().Dy()) 78 | } 79 | 80 | func convertToNRGBA(img image.Image) *image.NRGBA { 81 | if res, ok := img.(*image.NRGBA); ok { 82 | return res 83 | } 84 | 85 | res := image.NewNRGBA(img.Bounds()) 86 | draw.Draw(res, img.Bounds(), img, image.Pt(0, 0), draw.Src) 87 | 88 | return res 89 | } 90 | 91 | func extract(img *image.NRGBA, r image.Rectangle) *image.NRGBA { 92 | output := image.NewNRGBA(image.Rect(0, 0, r.Dx(), r.Dy())) 93 | 94 | for x := r.Min.X; x < r.Max.X; x++ { 95 | for y := r.Min.Y; y < r.Max.Y; y++ { 96 | index := y*img.Stride + x*4 97 | inputColor := img.Pix[index : index+4] 98 | 99 | index = (y-r.Min.Y)*output.Stride + (x-r.Min.X)*4 100 | output.Pix[index] = inputColor[0] 101 | output.Pix[index+1] = inputColor[1] 102 | output.Pix[index+2] = inputColor[2] 103 | output.Pix[index+3] = inputColor[3] 104 | } 105 | } 106 | 107 | return output 108 | } 109 | 110 | func scale(img *image.NRGBA, scale int) *image.NRGBA { 111 | if scale < 2 { 112 | return img 113 | } 114 | 115 | bounds := img.Bounds().Size() 116 | output := image.NewNRGBA(image.Rect(0, 0, bounds.X*scale, bounds.Y*scale)) 117 | 118 | for x := 0; x < bounds.X; x++ { 119 | for y := 0; y < bounds.Y; y++ { 120 | i := y*img.Stride + x*4 121 | color := img.Pix[i : i+4] 122 | 123 | for sx := 0; sx < scale; sx++ { 124 | for sy := 0; sy < scale; sy++ { 125 | i = (y*scale+sy)*output.Stride + (x*scale+sx)*4 126 | output.Pix[i] = color[0] 127 | output.Pix[i+1] = color[1] 128 | output.Pix[i+2] = color[2] 129 | output.Pix[i+3] = color[3] 130 | } 131 | } 132 | } 133 | } 134 | 135 | return output 136 | } 137 | 138 | func removeTransparency(img *image.NRGBA) *image.NRGBA { 139 | output := clone(img) 140 | 141 | for i, l := 0, len(output.Pix); i < l; i += 4 { 142 | output.Pix[i+3] = math.MaxUint8 143 | } 144 | 145 | return output 146 | } 147 | 148 | func flipHorizontal(src *image.NRGBA) *image.NRGBA { 149 | bounds := src.Bounds() 150 | output := image.NewNRGBA(bounds) 151 | 152 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 153 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 154 | index := y*src.Stride + x*4 155 | inputColor := src.Pix[index : index+4] 156 | 157 | index = y*output.Stride + (bounds.Max.X-x-1)*4 158 | output.Pix[index] = inputColor[0] 159 | output.Pix[index+1] = inputColor[1] 160 | output.Pix[index+2] = inputColor[2] 161 | output.Pix[index+3] = inputColor[3] 162 | } 163 | } 164 | 165 | return output 166 | } 167 | 168 | func fixTransparency(img *image.NRGBA) *image.NRGBA { 169 | checkColor := img.Pix[0:4] 170 | 171 | if checkColor[3] == 0 { 172 | return img 173 | } 174 | 175 | output := clone(img) 176 | 177 | for i, l := 0, len(output.Pix); i < l; i += 4 { 178 | if !isEqualSlice(checkColor, output.Pix[i:i+4]) { 179 | continue 180 | } 181 | 182 | output.Pix[i+3] = 0 183 | } 184 | 185 | return output 186 | } 187 | 188 | func clone(img *image.NRGBA) *image.NRGBA { 189 | bounds := img.Bounds() 190 | output := image.NewNRGBA(bounds) 191 | 192 | draw.Draw(output, bounds, img, zeroPoint, draw.Src) 193 | 194 | return output 195 | } 196 | 197 | func getSlimOffset(slim bool) int { 198 | if slim { 199 | return 1 200 | } 201 | 202 | return 0 203 | } 204 | 205 | func composite(dst, src *image.NRGBA, dx, dy int) { 206 | outputBounds := dst.Bounds() 207 | srcBounds := src.Bounds() 208 | 209 | for x := srcBounds.Min.X; x < srcBounds.Max.X; x++ { 210 | for y := srcBounds.Min.Y; y < srcBounds.Max.Y; y++ { 211 | if dx+x < outputBounds.Min.X || dy+y < outputBounds.Min.Y || dx+x >= outputBounds.Max.X || dy+y >= outputBounds.Max.Y { 212 | continue 213 | } 214 | 215 | index := y*src.Stride + x*4 216 | sourceColor := src.Pix[index : index+4] 217 | 218 | index = (dy+y)*dst.Stride + (dx+x)*4 219 | outputColor := dst.Pix[index : index+4] 220 | 221 | compositeColors(outputColor, sourceColor) 222 | } 223 | } 224 | } 225 | 226 | // This function is a whole mess of code that I do not want to touch, but it 227 | // seems to work very well. Most of this code was influenced by code in the 228 | // `go/x/image` package, but with a lot less redundancy. The color mixing 229 | // code was taken from the built-in Go method draw.Draw() from the 230 | // `image/draw` package. 231 | func compositeTransform(dst, src *image.NRGBA, m matrix2x2, outputX, outputY float64) { 232 | sourceBounds := src.Bounds() 233 | 234 | dstBounds := dst.Bounds() 235 | 236 | im := m.Inverse() 237 | dr := transformRect(m, src.Bounds()) 238 | dox, doy := translateCoordinatesWithMatrix(outputX, outputY, m) 239 | 240 | for boundX := dr.Min.X; boundX < dr.Max.X; boundX++ { 241 | for boundY := dr.Min.Y; boundY < dr.Max.Y; boundY++ { 242 | outputX, outputY := boundX+int(dox), boundY+int(doy) 243 | 244 | if outputX < dstBounds.Min.X || outputY < dstBounds.Min.Y || outputX >= dstBounds.Max.X || outputY >= dstBounds.Max.Y { 245 | continue 246 | } 247 | 248 | sourceX, sourceY := translateCoordinatesWithMatrix(float64(boundX), float64(boundY), im) 249 | 250 | if int(sourceX) < sourceBounds.Min.X || int(sourceY) < sourceBounds.Min.Y || int(sourceX) >= sourceBounds.Max.X || int(sourceY) >= sourceBounds.Max.Y { 251 | continue 252 | } 253 | 254 | index := int(sourceY)*src.Stride + int(sourceX)*4 255 | sourceColor := src.Pix[index : index+4] 256 | 257 | index = outputY*dst.Stride + outputX*4 258 | outputColor := dst.Pix[index : index+4] 259 | 260 | compositeColors(outputColor, sourceColor) 261 | } 262 | } 263 | } 264 | 265 | func compositeColors(outputColor, sourceColor []uint8) { 266 | sourceAlpha := uint32(sourceColor[3]) * 0x101 267 | 268 | alphaOffset := ((1<<16 - 1) - sourceAlpha) * 0x101 269 | 270 | outputColor[0] = uint8((uint32(outputColor[0])*alphaOffset/(1<<16-1) + (uint32(sourceColor[0]) * sourceAlpha / 0xff)) >> 8) 271 | outputColor[1] = uint8((uint32(outputColor[1])*alphaOffset/(1<<16-1) + (uint32(sourceColor[1]) * sourceAlpha / 0xff)) >> 8) 272 | outputColor[2] = uint8((uint32(outputColor[2])*alphaOffset/(1<<16-1) + (uint32(sourceColor[2]) * sourceAlpha / 0xff)) >> 8) 273 | outputColor[3] = uint8((uint32(outputColor[3])*alphaOffset/(1<<16-1) + sourceAlpha) >> 8) 274 | } 275 | 276 | func rotate90(img *image.NRGBA) *image.NRGBA { 277 | bounds := img.Bounds().Size() 278 | output := image.NewNRGBA(image.Rect(0, 0, bounds.Y, bounds.X)) 279 | 280 | for x := 0; x < bounds.X; x++ { 281 | for y := 0; y < bounds.Y; y++ { 282 | index := y*img.Stride + x*4 283 | inputColor := img.Pix[index : index+4] 284 | 285 | index = int(x)*output.Stride + int(y)*4 // Intentionally flipped X and Y 286 | output.Pix[index] = inputColor[0] 287 | output.Pix[index+1] = inputColor[1] 288 | output.Pix[index+2] = inputColor[2] 289 | output.Pix[index+3] = inputColor[3] 290 | } 291 | } 292 | 293 | return output 294 | } 295 | 296 | func rotate270(img *image.NRGBA) *image.NRGBA { 297 | bounds := img.Bounds().Size() 298 | output := image.NewNRGBA(image.Rect(0, 0, bounds.Y, bounds.X)) 299 | 300 | for x := 0; x < bounds.X; x++ { 301 | for y := 0; y < bounds.Y; y++ { 302 | index := y*img.Stride + x*4 303 | inputColor := img.Pix[index : index+4] 304 | 305 | index = (bounds.X-int(x)-1)*output.Stride + int(y)*4 // Intentionally flipped X and Y 306 | output.Pix[index] = inputColor[0] 307 | output.Pix[index+1] = inputColor[1] 308 | output.Pix[index+2] = inputColor[2] 309 | output.Pix[index+3] = inputColor[3] 310 | } 311 | } 312 | 313 | return output 314 | } 315 | 316 | func squareAndCenter(img *image.NRGBA) *image.NRGBA { 317 | var ( 318 | size int = max(img.Rect.Size().X, img.Rect.Size().Y) 319 | offsetX int = int((float64(size) - float64(img.Rect.Size().X)) / 2) 320 | offsetY int = int((float64(size) - float64(img.Rect.Size().Y)) / 2) 321 | 322 | output *image.NRGBA = image.NewNRGBA(image.Rect(0, 0, size, size)) 323 | ) 324 | 325 | composite(output, img, offsetX, offsetY) 326 | 327 | return output 328 | } 329 | 330 | func max[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](values ...T) T { 331 | result := values[0] 332 | 333 | for _, v := range values { 334 | if result > v { 335 | continue 336 | } 337 | 338 | result = v 339 | } 340 | 341 | return result 342 | } 343 | 344 | // Credit: https://github.com/LapisBlue/Lapitar/blob/55ede80ce4ebb5ecc2b968164afb40f61b4cc509/mc/uuid.go#L23 345 | func isEven(c uint8) bool { 346 | switch { 347 | case c >= '0' && c <= '9': 348 | return (c & 1) == 0 349 | case c >= 'a' && c <= 'f': 350 | return (c & 1) == 1 351 | default: 352 | panic(fmt.Errorf("invalid character: %c", c)) 353 | } 354 | } 355 | 356 | func isEqualSlice[T comparable](a, b []T) bool { 357 | if len(a) != len(b) { 358 | return false 359 | } 360 | 361 | for i, l := 0, len(a); i < l; i++ { 362 | if a[i] == b[i] { 363 | continue 364 | } 365 | 366 | return false 367 | } 368 | 369 | return true 370 | } 371 | --------------------------------------------------------------------------------