# pyskin

<p align="center">
  <img src="https://raw.githubusercontent.com/t-mart/skinpy/master/examples/render/lab_space.png" alt="isometric render" height=300>
</p>

A Python library for Minecraft skins.

- Load skins from a file, or start from scratch
- Index with 3D coordinates to get/set skin pixel color
- Operate at the skin level, the body part level, or even just one face
- Generate skin images for use in-game
- Render isometric ("angled/tilted view", like above) images of your skin
- Support for slim (Alex) and classic (Steve) skin models
- Full support for second layer (overlay) rendering

## Installation

```shell
pip install pyskin
```

## Quickstart

### Creating/Loading/Saving a skin

```python
from pyskin import Skin

# make a new skin
new_skin = Skin.new()
new_skin.to_image().save("blank.png")

# or load a skin from disk
loaded_skin = Skin.from_path("my_skin.png")
loaded_skin.to_image().save("copy.png")
```

### Rendering Isometric Images

You can render isometric images with the CLI tool:

```shell
pyskin render steve.png -o render.png
# see help with `pyskin render --help`
```

Or, here'e the API interface:

```python
from pyskin import Skin, Perspective

skin = Skin.from_path("steve.png")

# create a perspective from which to view the render
perspective = Perspective(
  x="left",
  y="front",
  z="up",
  scaling_factor=5, # bigger numbers mean bigger image
)

# save the render (overlay layer enabled by default)
skin.to_isometric_image(perspective).save("render.png")

# or disable overlay rendering
skin.to_isometric_image(perspective, render_overlay=False).save("render_base.png")
```

Outputted file:

![outputted file](https://github.com/t-mart/skinpy/raw/master/docs/steve-render.png)

### Pixel Indexing

```python
from pyskin import Skin

skin = Skin.from_path("steve.png")
magenta = (211, 54, 130, 255)  # RGBA

# get/set using entire skin's 3D coordinates
color = skin.get_color(4, 2, 0, "front") # somewhere on steve's right foot
print(f"Skin pixel at (4, 2, 0, 'front') was {color}")
skin.set_color(4, 2, 0, "front", magenta)

# get/set on just a head. coordinates become relative to just that part
color = skin.head.get_color(0, 1, 2, "left")
print(f"Head pixel at (0, 1, 2, 'left') was {color}")
skin.head.set_color(0, 1, 2, "left", magenta)

# or finally, just on one face. faces only have two dimensions
# NOTE: Face does not necessarily mean just a character's face! It just
# refers to the side of a cubiod, which all body parts are in Minecraft
color = skin.left_arm.up.get_color(5, 5)
print(f"Left arm up pixel at (5, 5) was {color}")
skin.head.set_color(0, 1, 2, "left", magenta)

skin.to_image().save("some_magenta.png")
```

Here's an animated visualization of equivalent ways to access a certain pixel:

<p>
  <img src="https://github.com/t-mart/skinpy/raw/master/examples/render/steve-index.gif" alt="indexing" height=500>
</p>

(This image was made with `examples/index.py`.)

### Pixel Enumeration

```python
from pyskin import Skin

skin = Skin.from_path("steve.png")

for (x, y, z), body_part_id, face_id, color in skin.enumerate_color():
  print(f"{x=}, {y=}, {z=}, {body_part_id=}, {face_id=}, {color=}")

for (x, y, z), face_id, color in skin.torso.enumerate_color():
  print(f"{x=}, {y=}, {z=}, {face_id=}, {color=}")

for (x, y), face_id, color in skin.torso.back.enumerate_color():
  print(f"{x=}, {y=}, {color=}")
```

### Slim Skins (Alex Model)

```python
from pyskin import Skin

# Load a slim skin (3-pixel wide arms)
slim_skin = Skin.from_path("alex_skin.png", slim=True)

# Create a new slim skin
new_slim_skin = Skin.new(slim=True)
```

### Overlay Layer (Second Layer)

```python
from pyskin import Skin

skin = Skin.from_path("skin_with_overlay.png")

# Access overlay body parts
head_overlay = skin.head_overlay
torso_overlay = skin.torso_overlay
left_arm_overlay = skin.left_arm_overlay
right_arm_overlay = skin.right_arm_overlay
left_leg_overlay = skin.left_leg_overlay
right_leg_overlay = skin.right_leg_overlay

# Get overlay part by ID
overlay = skin.get_overlay_part_for_id("head")

# Enumerate colors with overlay info
for (x, y, z), body_part_id, face_id, color, is_overlay in skin.enumerate_color_with_overlay():
    if is_overlay:
        print(f"Overlay pixel at ({x}, {y}, {z})")
```

## Coordinate system

pyskin uses a coordinate system with the origin at the left-down-front of the
skin **from the perspective of an observer looking at the skin**.

![coordinate system](https://github.com/t-mart/skinpy/raw/master/docs/coordsys.png)

## `FaceId`

In some methods, a `FaceId` type is asked for or provided. These are string literals:

- `up`
- `down`
- `left`
- `right`
- `front`
- `back`

## `BodyPartId`

Similarly, body parts are string literals under `BodyPartId`:

- `head`
- `torso`
- `right_arm`
- `left_arm`
- `right_leg`
- `left_leg`

Body parts that are "left" or "right" follow the same perspective as before: from the observer's point of view.

## API Reference

### Skin Class

#### Constructor Methods

| Method | Description |
|--------|-------------|
| `Skin.new(slim=False)` | Create a new blank skin |
| `Skin.filled(color, slim=False)` | Create a skin filled with a single color |
| `Skin.from_image(image, slim=False)` | Load a skin from a PIL Image |
| `Skin.from_path(path, slim=False)` | Load a skin from a file path |

#### Parameters

- `slim` (bool): Set to `True` for Alex-style slim arms (3 pixels wide instead of 4)

#### Body Parts (Base Layer)

- `skin.head` - Head body part
- `skin.torso` - Torso body part
- `skin.left_arm` - Left arm body part
- `skin.right_arm` - Right arm body part
- `skin.left_leg` - Left leg body part
- `skin.right_leg` - Right leg body part

#### Body Parts (Overlay Layer)

- `skin.head_overlay` - Head overlay (hat layer)
- `skin.torso_overlay` - Torso overlay (jacket layer)
- `skin.left_arm_overlay` - Left arm overlay (sleeve)
- `skin.right_arm_overlay` - Right arm overlay (sleeve)
- `skin.left_leg_overlay` - Left leg overlay (pants layer)
- `skin.right_leg_overlay` - Right leg overlay (pants layer)

#### Properties

| Property | Return Type | Description |
|----------|-------------|-------------|
| `skin.body_parts` | `tuple[BodyPart, ...]` | All base layer body parts |
| `skin.overlay_parts` | `tuple[BodyPart, ...]` | All overlay layer body parts |
| `skin.all_parts` | `tuple[BodyPart, ...]` | All body parts (base + overlay) |
| `skin.slim` | `bool` | Whether the skin uses slim arm dimensions |
| `skin.shape` | `tuple[int, int, int]` | The overall skin dimensions |

#### Methods

| Method | Description |
|--------|-------------|
| `skin.get_body_part_for_id(id)` | Get a base layer body part by ID |
| `skin.get_overlay_part_for_id(id)` | Get an overlay layer body part by ID |
| `skin.enumerate_color()` | Iterate over all base layer pixels |
| `skin.enumerate_color_with_overlay()` | Iterate over all pixels including overlay info |
| `skin.get_color(x, y, z, face)` | Get the color at a specific position |
| `skin.set_color(x, y, z, face, color)` | Set the color at a specific position |
| `skin.to_image()` | Convert the skin to a PIL Image |
| `skin.to_isometric_image(perspective, ...)` | Render an isometric view |

#### `to_isometric_image()` Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `perspective` | `Perspective` | required | The viewing angle |
| `background_color` | `tuple[int, int, int, int]` | `None` | RGBA background color |
| `render_overlay` | `bool` | `True` | Whether to render the overlay layer |

### Perspective Class

```python
from pyskin import Perspective

perspective = Perspective(
    x="left",      # or "right"
    y="front",     # or "back"
    z="up",        # or "down"
    scaling_factor=10,  # pixels per voxel
)
```

## Examples

You can find the skins/renders, and the code to produced them, in
[examples](./examples).

## Changelog

### v0.1.0

- Added support for slim (Alex) skin models with 3-pixel wide arms
- Added full support for second layer (overlay) rendering
- Added `overlay_parts`, `all_parts`, `get_overlay_part_for_id()` methods
- Added `enumerate_color_with_overlay()` for iterating both layers
- Added `render_overlay` parameter to `to_isometric_image()`
- Changed package name from `skinpy` to `pyskin`
