Загрузка и сохранение изображений в C: оптимальные библиотеки

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Начинающие программисты, изучающие язык C и обработку изображений
  • Профессионалы, желающие улучшить свои навыки работы с графикой на C
  • Разработчики встраиваемых систем и программисты, интересующиеся оптимизацией памяти и производительности

    Работа с изображениями в языке C часто вызывает панический ужас у начинающих программистов. Многие считают, что без использования C++ или более высокоуровневых языков обработка графики — это непреодолимый квест. Однако профессионалы знают: C предоставляет мощные инструменты для эффективной работы с изображениями, позволяя создавать быстрые и легковесные решения. Загрузка и сохранение графических файлов в C требует понимания некоторых ключевых концепций и выбора правильных библиотек, но при правильном подходе это становится рутинной задачей. 🖼️

Осваиваете программирование и хотите углубить знания не только в C, но и получить комплексные навыки разработки? Курс Java-разработки от Skypro — идеальное дополнение к вашему профессиональному набору инструментов. Java, как и C, позволяет эффективно работать с изображениями, но предлагает более высокоуровневый подход. Добавьте к своему арсеналу язык, востребованный на рынке, и станьте универсальным разработчиком, способным решать задачи любой сложности!

Основы работы с изображениями в языке C

Работа с изображениями в C начинается с понимания, что любое изображение — это просто структурированный набор данных. Без встроенных средств для работы с графикой, C требует четкого понимания форматов изображений и способов их представления в памяти.

Фундаментально, цифровое изображение — это двумерный массив пикселей, где каждый пиксель содержит информацию о цвете. В зависимости от формата изображения и глубины цвета, пиксель может занимать от одного бита (черно-белое изображение) до нескольких байтов (полноцветное изображение с альфа-каналом).

Тип изображения Бит на пиксель Описание
Черно-белое (1-битное) 1 Каждый пиксель может быть только черным или белым
Оттенки серого 8 256 оттенков серого от черного до белого
RGB 24 8 бит на каждый канал (красный, зеленый, синий)
RGBA 32 RGB + альфа-канал для прозрачности

При работе с изображениями в C необходимо учитывать следующие аспекты:

  • Формат файла — определяет структуру данных и способы сжатия (PNG, JPEG, BMP)
  • Размеры изображения — ширина и высота в пикселях
  • Глубина цвета — количество бит для хранения цвета одного пикселя
  • Порядок байтов — особенно важно при работе с многобайтными значениями
  • Управление памятью — выделение и освобождение памяти для данных изображения

Базовая структура для представления изображения в C может выглядеть так:

c
Скопировать код
typedef struct {
unsigned char* data; // Массив пикселей
int width; // Ширина изображения
int height; // Высота изображения
int channels; // Количество каналов (3 для RGB, 4 для RGBA)
} Image;

Важно понимать, что в языке C нет встроенных функций для работы с изображениями. Для чтения и записи файлов изображений требуются либо собственные реализации алгоритмов кодирования и декодирования, либо использование специализированных библиотек.

Пошаговый план для смены профессии

Библиотеки для управления изображениями в C

Экосистема C предлагает несколько проверенных библиотек для обработки изображений, каждая со своими преимуществами и особенностями. 📚

Александр Петров, руководитель отдела встраиваемых систем

Три года назад мы разрабатывали программное обеспечение для промышленного сканера, работающего на микроконтроллере с ограниченными ресурсами. Требовалось обрабатывать снимки в высоком разрешении, сохраняя их в различных форматах. Попытки использовать тяжеловесные библиотеки с множеством зависимостей приводили к нехватке памяти и непредсказуемому поведению устройства.

Решением стала библиотека stb_image — минималистичное решение, состоящее всего из одного заголовочного файла. Мы смогли интегрировать только нужные функции, что сократило размер кода на 60% по сравнению с libpng и libjpeg. Производительность выросла на 40%, а объем используемой памяти уменьшился вдвое. Сегодня это стандартное решение для всех наших встраиваемых проектов с ограниченными ресурсами.

При выборе библиотеки важно учитывать поддерживаемые форматы, производительность, размер кода и сложность интеграции:

Библиотека Поддерживаемые форматы Особенности Размер Лицензия
libpng PNG Высокая производительность, полная поддержка спецификации PNG Средний libpng license (аналог zlib)
libjpeg / libjpeg-turbo JPEG Стандартная библиотека для JPEG, turbo-версия с SIMD-оптимизациями Средний IJG license / BSD
stb_image JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC Минималистичная, "header-only" библиотека Маленький Public Domain / MIT
FreeImage BMP, JPEG, TIFF, PNG, и многие другие Универсальный API для различных форматов Большой FIPL (FreeImage Public License)
ImageMagick Более 200 форматов Полнофункциональное решение для профессиональной обработки Очень большой Apache 2.0

Для большинства проектов оптимальным выбором являются:

  • stb_image — для простых задач и встраиваемых систем с ограниченными ресурсами
  • libpng + libjpeg — для проектов, требующих полной поддержки форматов и высокой производительности
  • FreeImage — для приложений, работающих с множеством форматов через единый API

Установка библиотек выполняется стандартными средствами управления пакетами. Например, для Ubuntu:

Bash
Скопировать код
# Для libpng и libjpeg
sudo apt-get install libpng-dev libjpeg-dev

# Для FreeImage
sudo apt-get install libfreeimage-dev

# Для ImageMagick
sudo apt-get install libmagickwand-dev

Для stb_image достаточно скачать единственный заголовочный файл с GitHub и включить его в проект.

Реализация загрузки изображений разных форматов

Разработка кода для загрузки изображений в C требует внимания к деталям и понимания особенностей каждого формата. Рассмотрим наиболее популярные подходы и примеры кода. 🔍

Загрузка изображений с использованием stb_image

Библиотека stb_image предлагает самый простой способ загрузки изображений различных форматов. Достаточно включить один заголовочный файл и определить реализацию перед включением:

c
Скопировать код
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

// Функция для загрузки изображения
unsigned char* load_image(const char* filename, int* width, int* height, int* channels) {
unsigned char* data = stbi_load(filename, width, height, channels, 0);
if (data == NULL) {
fprintf(stderr, "Ошибка при загрузке изображения: %s\n", stbi_failure_reason());
return NULL;
}
return data;
}

// Пример использования
int main() {
int width, height, channels;
unsigned char* img = load_image("example.jpg", &width, &height, &channels);
if (!img) {
return 1;
}

printf("Изображение загружено: %dx%d, %d каналов\n", width, height, channels);

// Здесь можно обрабатывать изображение

// Освобождение памяти
stbi_image_free(img);
return 0;
}

Преимущество stb_image — универсальность и простота. Одна функция автоматически определяет формат и загружает изображение в единую структуру данных.

Загрузка PNG с использованием libpng

Для более полного контроля над процессом загрузки, особенно при работе с PNG-файлами, libpng предоставляет более детальный API:

c
Скопировать код
#include <png.h>
#include <stdio.h>
#include <stdlib.h>

// Функция загрузки PNG-изображения
unsigned char* load_png(const char* filename, int* width, int* height) {
FILE* fp = fopen(filename, "rb");
if (!fp) {
return NULL;
}

png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) {
fclose(fp);
return NULL;
}

png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_read_struct(&png, NULL, NULL);
fclose(fp);
return NULL;
}

if (setjmp(png_jmpbuf(png))) {
png_destroy_read_struct(&png, &info, NULL);
fclose(fp);
return NULL;
}

png_init_io(png, fp);
png_read_info(png, info);

*width = png_get_image_width(png, info);
*height = png_get_image_height(png, info);
png_byte color_type = png_get_color_type(png, info);
png_byte bit_depth = png_get_bit_depth(png, info);

// Преобразование к 8 бит на канал RGBA, если необходимо
if (bit_depth == 16)
png_set_strip_16(png);

if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(png);

if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
png_set_expand_gray_1_2_4_to_8(png);

if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);

if (color_type == PNG_COLOR_TYPE_RGB ||
color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_PALETTE)
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);

if (color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png);

png_read_update_info(png, info);

int rowbytes = png_get_rowbytes(png, info);
unsigned char* image_data = (unsigned char*)malloc(*height * rowbytes);
png_bytep* row_pointers = (png_bytep*)malloc(*height * sizeof(png_bytep));

for (int y = 0; y < *height; y++)
row_pointers[y] = image_data + y * rowbytes;

png_read_image(png, row_pointers);

fclose(fp);
free(row_pointers);
png_destroy_read_struct(&png, &info, NULL);

return image_data;
}

Михаил Соколов, разработчик систем компьютерного зрения

В одном из медицинских проектов мы столкнулись с необходимостью загружать и анализировать рентгеновские снимки высокого разрешения. Изначально мы использовали универсальную библиотеку, но её производительность оказалась недостаточной — на загрузку 20-мегапиксельного изображения уходило до 700 мс.

Переписав код с использованием libjpeg-turbo с ручной оптимизацией буферов и прямым доступом к строкам изображения, мы сократили время загрузки до 150 мс. Ключевым фактором стало использование SIMD-инструкций для параллельной обработки данных и асинхронное чтение блоков изображения с диска.

Эта оптимизация позволила врачам просматривать серии из сотен снимков без ощутимых задержек, что было критично для диагностического процесса. Дополнительным бонусом стало снижение нагрузки на процессор, что позволило выполнять предварительный анализ изображений в фоновом режиме.

При загрузке JPEG-файлов с использованием libjpeg рекомендуется использовать постепенную загрузку для больших изображений:

c
Скопировать код
#include <stdio.h>
#include <stdlib.h>
#include <jpeglib.h>

unsigned char* load_jpeg(const char* filename, int* width, int* height, int* channels) {
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;

FILE* infile = fopen(filename, "rb");
if (!infile) {
return NULL;
}

cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, infile);

jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo);

*width = cinfo.output_width;
*height = cinfo.output_height;
*channels = cinfo.output_components;

int row_stride = (*width) * (*channels);
unsigned char* buffer = (unsigned char*)malloc((*height) * row_stride);

while (cinfo.output_scanline < cinfo.output_height) {
unsigned char* row_pointer = buffer + (cinfo.output_scanline * row_stride);
jpeg_read_scanlines(&cinfo, &row_pointer, 1);
}

jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(infile);

return buffer;
}

Для проектов, требующих поддержки множества форматов с единым API, FreeImage предлагает более высокоуровневый подход:

c
Скопировать код
#include <FreeImage.h>

// Необходимо вызвать FreeImage_Initialise() при запуске программы
// и FreeImage_DeInitialise() при завершении

FIBITMAP* load_image_freeimage(const char* filename) {
FREE_IMAGE_FORMAT format = FreeImage_GetFileType(filename, 0);
if (format == FIF_UNKNOWN) {
format = FreeImage_GetFIFFromFilename(filename);
}
if (format == FIF_UNKNOWN) {
return NULL;
}

FIBITMAP* bitmap = FreeImage_Load(format, filename, 0);
if (!bitmap) {
return NULL;
}

// Преобразование к 32-битному RGBA для унификации
FIBITMAP* temp = bitmap;
bitmap = FreeImage_ConvertTo32Bits(temp);
FreeImage_Unload(temp);

return bitmap;
}

При выборе метода загрузки изображений следует учитывать следующие факторы:

  • Требуемые форматы — для узкоспециализированных задач лучше использовать специфичные библиотеки (libpng, libjpeg)
  • Ресурсы системы — для встраиваемых систем предпочтительнее легковесные решения (stb_image)
  • Сложность кода — универсальные API (FreeImage) упрощают поддержку кода
  • Производительность — специализированные библиотеки часто быстрее, особенно с дополнительными оптимизациями

Техники сохранения изображений в файлы

Сохранение изображений в файлы требует понимания форматов и их особенностей. Рассмотрим наиболее эффективные подходы для различных библиотек. 💾

Наиболее простой способ сохранения изображений реализован в stbimagewrite:

c
Скопировать код
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

int save_image(const char* filename, int width, int height, int channels, 
unsigned char* data) {
// Определение формата по расширению
const char* ext = strrchr(filename, '.');
if (!ext) return 0;

if (strcmp(ext, ".png") == 0) {
// Сохранение в PNG (сжатие 8 из 10)
return stbi_write_png(filename, width, height, channels, data, width * channels);
}
else if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) {
// Сохранение в JPEG (качество 90 из 100)
return stbi_write_jpg(filename, width, height, channels, data, 90);
}
else if (strcmp(ext, ".bmp") == 0) {
return stbi_write_bmp(filename, width, height, channels, data);
}
else if (strcmp(ext, ".tga") == 0) {
return stbi_write_tga(filename, width, height, channels, data);
}

return 0; // Неподдерживаемый формат
}

Для более гибкого контроля при сохранении PNG с использованием libpng:

c
Скопировать код
#include <png.h>

int save_png(const char* filename, int width, int height, int channels,
unsigned char* data, int compression_level) {
FILE *fp = fopen(filename, "wb");
if (!fp) return 0;

png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) {
fclose(fp);
return 0;
}

png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_write_struct(&png, NULL);
fclose(fp);
return 0;
}

if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
fclose(fp);
return 0;
}

png_init_io(png, fp);

// Установка уровня сжатия (0-9, где 0 – без сжатия, 9 – максимальное сжатие)
png_set_compression_level(png, compression_level);

// Настройка параметров изображения
int color_type = channels == 4 ? PNG_COLOR_TYPE_RGBA : PNG_COLOR_TYPE_RGB;
png_set_IHDR(png, info, width, height, 8, color_type,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);

// Запись заголовка
png_write_info(png, info);

// Подготовка массива указателей на строки изображения
png_bytep* row_pointers = (png_bytep*)malloc(height * sizeof(png_bytep));
int stride = width * channels;

for (int y = 0; y < height; y++) {
row_pointers[y] = data + y * stride;
}

// Запись данных изображения
png_write_image(png, row_pointers);
png_write_end(png, NULL);

// Освобождение ресурсов
free(row_pointers);
png_destroy_write_struct(&png, &info);
fclose(fp);

return 1;
}

Для сохранения JPEG с контролем качества, используя libjpeg:

c
Скопировать код
#include <jpeglib.h>

int save_jpeg(const char* filename, int width, int height, int channels,
unsigned char* data, int quality) {
// JPEG поддерживает только 1 или 3 канала (grayscale или RGB)
if (channels != 1 && channels != 3) return 0;

struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE* outfile;
JSAMPROW row_pointer[1];

outfile = fopen(filename, "wb");
if (!outfile) return 0;

cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
jpeg_stdio_dest(&cinfo, outfile);

cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = channels;
cinfo.in_color_space = channels == 3 ? JCS_RGB : JCS_GRAYSCALE;

jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
jpeg_start_compress(&cinfo, TRUE);

int stride = width * channels;
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = &data[cinfo.next_scanline * stride];
jpeg_write_scanlines(&cinfo, row_pointer, 1);
}

jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
fclose(outfile);

return 1;
}

При сохранении изображений важно учитывать следующие аспекты:

  • Сжатие и качество — выбор оптимального баланса между размером файла и качеством изображения
  • Формат — PNG для изображений с прозрачностью, JPEG для фотографий, BMP для полного сохранения данных
  • Каналы — проверка соответствия каналов допустимым для формата (JPEG не поддерживает альфа-канал)
  • Метаданные — при необходимости добавление EXIF, ICC-профилей и других метаданных

Для универсального сохранения с поддержкой различных форматов через единый API, FreeImage предлагает простое решение:

c
Скопировать код
#include <FreeImage.h>

int save_image_freeimage(FIBITMAP* bitmap, const char* filename, int flags) {
FREE_IMAGE_FORMAT format = FreeImage_GetFIFFromFilename(filename);
if (format == FIF_UNKNOWN) {
return 0;
}

if (!FreeImage_FIFSupportsWriting(format)) {
return 0;
}

return FreeImage_Save(format, bitmap, filename, flags);
}

// Пример использования
int main() {
FreeImage_Initialise();

FIBITMAP* bitmap = FreeImage_Allocate(800, 600, 24);
// Заполнение bitmap данными...

// Сохранение в JPEG с качеством 90%
save_image_freeimage(bitmap, "output.jpg", JPEG_QUALITYGOOD);

// Сохранение в PNG с максимальным сжатием
save_image_freeimage(bitmap, "output.png", PNG_Z_BEST_COMPRESSION);

FreeImage_Unload(bitmap);
FreeImage_DeInitialise();
return 0;
}

Оптимизация кода при обработке графических данных

Оптимизация кода для работы с изображениями в C требует сочетания алгоритмических улучшений и грамотного управления ресурсами. ⚡

Одним из главных ограничений при работе с изображениями является размер памяти. Рассмотрим техники эффективного использования памяти:

  • Строчная обработка — загрузка и обработка изображения построчно, без хранения всего изображения в памяти
  • Преобразование на лету — изменение формата или размера изображения в процессе загрузки
  • Повторное использование буферов — создание пула буферов для многократного использования
  • Управление памятью вручную — предварительное выделение и явное освобождение памяти

Пример оптимизированной загрузки большого изображения порциями:

c
Скопировать код
// Функция для обработки изображения построчно
int process_image_in_chunks(const char* filename, 
void (*process_row)(unsigned char* row, int width, int channels)) {
int width, height, channels;

// Открыть файл и прочитать заголовок для получения размеров
FILE* f = fopen(filename, "rb");
if (!f) return 0;

stbi__context s;
stbi__start_file(&s, f);

int result = stbi__jpeg_info(&s, &width, &height, &channels);
if (!result) {
fclose(f);
return 0;
}

// Сбросить позицию файла и инициализировать декодер JPEG
fseek(f, 0, SEEK_SET);

struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;

cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, f);

jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo);

int row_stride = cinfo.output_width * cinfo.output_components;
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)
((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);

// Обработка по одной строке за раз
while (cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines(&cinfo, buffer, 1);
process_row(buffer[0], cinfo.output_width, cinfo.output_components);
}

jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(f);

return 1;
}

Для повышения скорости обработки данных в C можно использовать различные техники оптимизации:

Техника оптимизации Описание Применимость Потенциальный прирост
SIMD инструкции Использование SSE/AVX/NEON для параллельной обработки пикселей Фильтры, преобразования цветового пространства 2x-8x
Многопоточность Распределение обработки между потоками Большие изображения, независимые операции Почти линейный рост с числом ядер
Кэширование данных Переупорядочивание доступа для улучшения локальности Итеративные алгоритмы, обработка плиткой 1.5x-3x
Оптимизация алгоритмов Снижение вычислительной сложности Всегда применима Зависит от алгоритма
Оптимизация памяти Выравнивание данных, минимизация копирования Всегда применима 1.2x-2x

Пример использования SIMD-инструкций для ускорения конвертации RGB в оттенки серого:

c
Скопировать код
#include <immintrin.h> // Для SSE/AVX инструкций

// Неоптимизированная версия
void rgb_to_gray_scalar(unsigned char* rgb, unsigned char* gray, int pixel_count) {
for (int i = 0; i < pixel_count; i++) {
int r = rgb[i*3];
int g = rgb[i*3 + 1];
int b = rgb[i*3 + 2];
// Стандартные коэффициенты для преобразования RGB в оттенки серого
gray[i] = (unsigned char)(0.299f * r + 0.587f * g + 0.114f * b);
}
}

// Оптимизированная версия с использованием SSE
void rgb_to_gray_sse(unsigned char* rgb, unsigned char* gray, int pixel_count) {
// Коэффициенты для преобразования RGB в серый
__m128 coeffs = _mm_setr_ps(0.299f, 0.587f, 0.114f, 0.0f);

int i = 0;
for (; i <= pixel_count – 4; i += 4) {
// Загружаем 12 байтов RGB (4 пикселя)
__m128i rgb0 = _mm_loadu_si128((__m128i*)(rgb + i*3));
__m128i rgb1 = _mm_loadu_si128((__m128i*)(rgb + i*3 + 12));

// Распаковываем и конвертируем в float
__m128i r0g0b0r1 = _mm_unpacklo_epi8(rgb0, _mm_setzero_si128());
__m128i g1b1r2g2 = _mm_unpackhi_epi8(rgb0, _mm_setzero_si128());
__m128i b2r3g3b3 = _mm_unpacklo_epi8(rgb1, _mm_setzero_si128());

// Дальнейшая распаковка и умножение на коэффициенты
// ... (сложная реализация с шафлами и умножениями опущена)

// Запись результата
// ... (упаковка обратно в байты и сохранение)
}

// Обработка оставшихся пикселей
for (; i < pixel_count; i++) {
int r = rgb[i*3];
int g = rgb[i*3 + 1];
int b = rgb[i*3 + 2];
gray[i] = (unsigned char)(0.299f * r + 0.587f * g + 0.114f * b);
}
}

Важные рекомендации для оптимизации кода при работе с изображениями:

  • Профилирование — определение узких мест перед оптимизацией
  • Выбор подходящих структур данных — использование непрерывных массивов вместо связных структур
  • Предварительная обработка — подготовка таблиц поиска для сложных вычислений
  • Использование кэша процессора — организация доступа к памяти для максимального использования кэша
  • Асинхронные операции — разделение I/O и вычислений для параллельного выполнения

Мощь языка C при работе с изображениями кроется в сочетании низкоуровневого контроля и высокой производительности. Правильный выбор библиотеки — только первый шаг; реальные преимущества приходят с грамотной оптимизацией и пониманием принципов работы с памятью. Независимо от сложности проекта — от встраиваемых систем до высоконагруженных серверов обработки графики — C предоставляет инструменты для создания эффективных решений. Освоив техники, описанные в этой статье, вы сможете разрабатывать код, который не только правильно обрабатывает изображения, но делает это максимально эффективно.

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какая библиотека используется для работы с PNG изображениями?
1 / 5

Загрузка...