diff --git a/pkg/images/imgconv/default_format.go b/pkg/images/imgconv/default_format.go
new file mode 100644
index 0000000000000000000000000000000000000000..d57a97020ec815d19471cfffa6125c1db6acb166
--- /dev/null
+++ b/pkg/images/imgconv/default_format.go
@@ -0,0 +1,28 @@
+package imgconv
+
+import (
+	"image"
+	"image/gif"
+	"image/jpeg"
+	"image/png"
+	"io"
+
+	"golang.org/x/image/bmp"
+	"golang.org/x/image/tiff"
+)
+
+const (
+	JPEG = "jpeg"
+	PNG  = "png"
+	GIF  = "gif"
+	TIFF = "tiff"
+	BMP  = "bmp"
+)
+
+func init() {
+	RegisterFormatEncoder(JPEG, func(w io.Writer, img image.Image) error { return jpeg.Encode(w, img, nil) })
+	RegisterFormatEncoder(PNG, func(w io.Writer, img image.Image) error { return png.Encode(w, img) })
+	RegisterFormatEncoder(GIF, func(w io.Writer, img image.Image) error { return gif.Encode(w, img, nil) })
+	RegisterFormatEncoder(TIFF, func(w io.Writer, img image.Image) error { return tiff.Encode(w, img, nil) })
+	RegisterFormatEncoder(BMP, func(w io.Writer, img image.Image) error { return bmp.Encode(w, img) })
+}
diff --git a/pkg/images/imgconv/imgconv.go b/pkg/images/imgconv/imgconv.go
new file mode 100644
index 0000000000000000000000000000000000000000..9f00fcb8676bffeda1fc9de3883adf4b806c1fba
--- /dev/null
+++ b/pkg/images/imgconv/imgconv.go
@@ -0,0 +1,65 @@
+package imgconv
+
+import (
+	"image"
+	"io"
+	"os"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+)
+
+var builtinFormats = map[string]string{
+	"jpg": "jpeg",
+	"tif": "tiff",
+}
+
+var defaultFormatEncoderRegistry = make(map[string]EncodeFunc)
+
+type EncodeFunc func(w io.Writer, img image.Image) error
+
+func RegisterFormatEncoder(format string, fn EncodeFunc) {
+	defaultFormatEncoderRegistry[format] = fn
+}
+
+func Encode(w io.Writer, format string, img image.Image) error {
+	encoder, ok := defaultFormatEncoderRegistry[format]
+	if !ok {
+		return errors.Errorf("unknown format: %s", format)
+	}
+	err := encoder(w, img)
+	if err != nil {
+		return errors.Wrap(err, "encode image")
+	}
+	return nil
+}
+
+func Decode(r io.Reader) (image.Image, string, error) {
+	img, ext, err := image.Decode(r)
+	if err != nil {
+		return nil, "", errors.Wrap(err, "decode image")
+	}
+	return img, ext, nil
+}
+
+func Open(filename string) (image.Image, string, error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return nil, "", errors.Wrap(err, "open file")
+	}
+	defer file.Close()
+	img, ext, err := Decode(file)
+	if err != nil {
+		return nil, "", errors.Wrap(err, "decode file")
+	}
+	return img, ext, nil
+}
+
+func NormalizeFormat(format string) string {
+	format = strings.ToLower(format)
+	format = strings.TrimPrefix(format, ".")
+	if v, ok := builtinFormats[format]; ok {
+		return v
+	}
+	return format
+}
diff --git a/pkg/images/imgconv/imgconv_test.go b/pkg/images/imgconv/imgconv_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..4ee0f21950f78fbcb76109f127003ad64ff0ea07
--- /dev/null
+++ b/pkg/images/imgconv/imgconv_test.go
@@ -0,0 +1,142 @@
+package imgconv
+
+import (
+	"bytes"
+	"image"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestOpen(t *testing.T) {
+	_, ext, err := Open("testdata/1.jpeg")
+	if err != nil {
+		return
+	}
+	require.NoError(t, err)
+	require.Equal(t, JPEG, ext)
+}
+
+func TestNormalizeFormat(t *testing.T) {
+	require.Equal(t, NormalizeFormat("jpg"), JPEG)
+	require.Equal(t, NormalizeFormat("png"), PNG)
+	require.Equal(t, NormalizeFormat("tif"), TIFF)
+	require.Equal(t, NormalizeFormat("any"), "any")
+	require.Equal(t, NormalizeFormat(".jpg"), JPEG)
+	require.Equal(t, NormalizeFormat(".gif"), GIF)
+	require.Equal(t, NormalizeFormat(".bmp"), BMP)
+	require.Equal(t, NormalizeFormat(".any"), "any")
+}
+
+func TestEncode(t *testing.T) {
+	var tests = []struct {
+		name    string
+		input   string
+		wantErr bool
+	}{
+		{
+			name:    "unknown format",
+			input:   "go",
+			wantErr: true,
+		},
+		{
+			name:    "jpeg format",
+			input:   JPEG,
+			wantErr: false,
+		},
+		{
+			name:    "png format",
+			input:   PNG,
+			wantErr: false,
+		},
+		{
+			name:    "gif format",
+			input:   GIF,
+			wantErr: false,
+		},
+		{
+			name:    "tiff format",
+			input:   TIFF,
+			wantErr: false,
+		},
+		{
+			name:    "bmp format",
+			input:   BMP,
+			wantErr: false,
+		},
+	}
+	img := image.NewRGBA(image.Rect(0, 0, 10, 10))
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			buf := new(bytes.Buffer)
+			err := Encode(buf, tt.input, img)
+			if tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestDecode(t *testing.T) {
+	var tests = []struct {
+		name    string
+		input   string
+		output  string
+		wantErr bool
+	}{
+		{
+			name:    "unknown format",
+			input:   "testdata/1.go",
+			wantErr: true,
+		},
+		{
+			name:    "jpeg format",
+			input:   "testdata/1.jpeg",
+			output:  JPEG,
+			wantErr: false,
+		},
+		{
+			name:    "png format",
+			input:   "testdata/1.png",
+			output:  PNG,
+			wantErr: false,
+		},
+		{
+			name:    "gif format",
+			input:   "testdata/1.gif",
+			output:  GIF,
+			wantErr: false,
+		},
+		{
+			name:    "tiff format",
+			input:   "testdata/1.tiff",
+			output:  TIFF,
+			wantErr: false,
+		},
+		{
+			name:    "bmp format",
+			input:   "testdata/1.bmp",
+			output:  BMP,
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			file, err := os.Open(tt.input)
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer file.Close()
+			_, ext, err := Decode(file)
+			if tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				require.Equal(t, tt.output, ext)
+			}
+		})
+	}
+}
diff --git a/pkg/images/imgconv/testdata/1.bmp b/pkg/images/imgconv/testdata/1.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..1a4a8d81288b72efbe3ccfc90887b86f897b137e
Binary files /dev/null and b/pkg/images/imgconv/testdata/1.bmp differ
diff --git a/pkg/images/imgconv/testdata/1.gif b/pkg/images/imgconv/testdata/1.gif
new file mode 100644
index 0000000000000000000000000000000000000000..73889f7e555aaf01ceb121d2b700a1b6ffd6428c
Binary files /dev/null and b/pkg/images/imgconv/testdata/1.gif differ
diff --git a/pkg/images/imgconv/testdata/1.go b/pkg/images/imgconv/testdata/1.go
new file mode 100644
index 0000000000000000000000000000000000000000..c8eb7f369645cb9cf399939b2297f06f937daace
--- /dev/null
+++ b/pkg/images/imgconv/testdata/1.go
@@ -0,0 +1,3 @@
+package testdata
+
+// hi
diff --git a/pkg/images/imgconv/testdata/1.jpeg b/pkg/images/imgconv/testdata/1.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..8348e38780de3332ba2e1d39fdb81fd8bba92389
Binary files /dev/null and b/pkg/images/imgconv/testdata/1.jpeg differ
diff --git a/pkg/images/imgconv/testdata/1.png b/pkg/images/imgconv/testdata/1.png
new file mode 100644
index 0000000000000000000000000000000000000000..d047fa256d11a29ce71b27ae123a22a1870f084d
Binary files /dev/null and b/pkg/images/imgconv/testdata/1.png differ
diff --git a/pkg/images/imgconv/testdata/1.tiff b/pkg/images/imgconv/testdata/1.tiff
new file mode 100644
index 0000000000000000000000000000000000000000..ca37358818808f0da0a11a3a9396e16035a04652
Binary files /dev/null and b/pkg/images/imgconv/testdata/1.tiff differ
diff --git a/pkg/images/imgconv/testdata/1.webp b/pkg/images/imgconv/testdata/1.webp
new file mode 100644
index 0000000000000000000000000000000000000000..122741b605f3121d393829ffb5b7a0924db13c86
Binary files /dev/null and b/pkg/images/imgconv/testdata/1.webp differ
diff --git a/pkg/images/imgconv/webp.go b/pkg/images/imgconv/webp.go
new file mode 100644
index 0000000000000000000000000000000000000000..0358d9ca76240229ac274e3e5751e332dd378127
--- /dev/null
+++ b/pkg/images/imgconv/webp.go
@@ -0,0 +1,23 @@
+//go:build webp
+
+package imgconv
+
+import (
+	"image"
+	"io"
+
+	"github.com/bep/gowebp/libwebp"
+	"github.com/bep/gowebp/libwebp/webpoptions"
+
+	// Нужно включать в сборку для вызова регистрации декодера в стандартном
+	// пакете "images" для декодирования файлов webp.
+	_ "golang.org/x/image/webp"
+)
+
+const (
+	WEBP = "webp"
+)
+
+func init() {
+	RegisterFormatEncoder(WEBP, func(w io.Writer, img image.Image) error { return libwebp.Encode(w, img, webpoptions.EncodingOptions{}) })
+}
diff --git a/pkg/images/imgconv/webp_test.go b/pkg/images/imgconv/webp_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..4574bfbd66593303a44fa2cd1ffca160786bf87b
--- /dev/null
+++ b/pkg/images/imgconv/webp_test.go
@@ -0,0 +1,30 @@
+//go:build webp
+
+package imgconv
+
+import (
+	"bytes"
+	"image"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestEncodeWebP(t *testing.T) {
+	img := image.NewRGBA(image.Rect(0, 0, 10, 10))
+	buf := new(bytes.Buffer)
+	err := Encode(buf, WEBP, img)
+	require.NoError(t, err)
+}
+
+func TestDecodeWebP(t *testing.T) {
+	file, err := os.Open("testdata/1.webp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer file.Close()
+	_, ext, err := Decode(file)
+	require.NoError(t, err)
+	require.Equal(t, WEBP, ext)
+}