Commit e3a275f5 authored by Igor Zhirkov's avatar Igor Zhirkov
Browse files

update

parent 0399167a
......@@ -45,14 +45,14 @@ struct bmp_header __attribute__((packed))
## Padding
Если ширина изображения в пикселах кратна четырём, то строчки идут одна за другой без пропусков.
Если ширина не кратна четырём, то она дополняется мусорными байтами до ближайшего числа байтов, кратного четырём.
Если ширина изображения в байтах кратна четырём, то строчки идут одна за другой без пропусков.
Если ширина не кратна четырём, то она дополняется мусорными байтами до ближайшего числа, кратного четырём.
Эти байты называются *padding*.
Пример:
1. Изображение имеет ширину 12 пикселей = 12 * 3 байт = 36 байт. Ширина кратна четырём, каждая следующая строчка начинается сразу после предыдущей.
2. Изображение имеет ширину 5 пикселей. 5 * 3 = 15 байт, ближайшее число кратное четырём (округление вверх) это 16. После каждой строчки будет отступ в один мусорный байт перед началом следующей.
2. Изображение имеет ширину 5 пикселей. 5 * 3 = 15 байт, ближайшее число кратное четырём (округление вверх) это 16. После каждой строчки будет отступ в 16-15 = 1 байт перед началом следующей.
Обратите внимание: отступы в *байтах*, не в пикселях.
......@@ -86,25 +86,35 @@ struct bmp_header
#pragma pack(pop)
```
Объяснение этого прочтите находится на страницах 235–239 учебника.
# Об архитектуре
Программа разделена на модули; каждый модуль это `.c` файл, который становится файлом с расширением `.o`.
Продуманная архитектура приложения в каждом конкретном модуле минимизирует знания о других модулях по следующим причинам:
- Когда программист работает над одним модулем (разрабатывает, модифицирует, ищет ошибки), ему проще не держать в голове знания про всю остальную программу.
- Пусть модуль A не использует определения из модуля B, но имеет к ним доступ. Разумеется, можно как угодно менять B и это не скажется на A.
Однако есть шанс, что автор программы или кто-то из будущих соавторов может использовать в модуле A определения из B — ведь к ним есть доступ.
Это установит жёсткую связь между A и B, и будет нельзя больше свободно менять B не влияя на A. Программы являются сложными системами, и мы хотим иметь минимум связей между их элементами, иначе модификация (и исправление ошибок) будут требовать постоянной модификации не одной, а многих частей программы.
Продуманная архитектура приложения в каждом конкретном модуле минимизирует
знания о других модулях по следующим причинам:
- Когда программист работает над одним модулем (разрабатывает, модифицирует,
ищет ошибки), ему проще не держать в голове знания про всю остальную
программу. Скрытие информации про другие модули позволяет ему не брать в
голову детали их внутреннего устройства.
- Пусть модуль A не использует определения из модуля B, но имеет к ним доступ.
Разумеется, можно как угодно менять B и это не скажется на A.
Однако есть шанс, что автор программы или кто-то из будущих соавторов может
использовать в модуле A определения из B — ведь к ним есть доступ. Это
установит жёсткую связь между A и B, и будет нельзя больше свободно менять B не
влияя на A. Программы являются сложными системами, и мы хотим иметь минимум
связей между их элементами, иначе модификация (и исправление ошибок) будут
требовать постоянной модификации не одной, а многих частей программы.
В нашем случае в программе разумно выделить несколько частей.
## Часть 1: Внутренний формат
Описание внутреннего представления картинки `struct image`, очищенное от деталей формата, и функции для работы с ним: создание, деинициализация и т.д.
Описание внутреннего представления картинки `struct image`, очищенное от
деталей формата, и функции для работы с ним: создание, деинициализация и т.д.
```c
struct image {
......@@ -117,62 +127,73 @@ struct bmp_header
## Часть 2: Входные форматы
Каждый входной формат описывается в отдельном модуле; они предоставляют функции для считывания файлов разных форматов в `struct image` и для записи на диск в тех же форматах.
Каждый входной формат описывается в отдельном модуле; они предоставляют функции
для считывания файлов разных форматов в `struct image` и для записи на диск в
тех же форматах.
Эти модули знают про модуль, описывающий `struct image`, но ничего не знают про трансформации. Поэтому можно будет добавлять новые трансформации не переписывая код для входных форматов.
Эти модули знают про модуль, описывающий `struct image`, но ничего не знают про
трансформации. Поэтому можно будет добавлять новые трансформации не переписывая
код для входных форматов.
Как только мы считали изображение во внутренний формат, мы должны забыть, из какого формата оно было считано! Именно поэтому в `struct image` оставлен только самый минимум деталей изображения (размеры), и никаких частей bmp-заголовка.
Для BMP начать можно с:
Как только мы считали изображение во внутренний формат, мы должны забыть, из
какого формата оно было считано! Именно поэтому в `struct image` оставлен
только самый минимум деталей изображения (размеры), и никаких частей
bmp-заголовка. Для BMP начать можно с:
```c
/* deserializer */
enum read_status {
READ_OK = 0,
READ_INVALID_SIGNATURE,
READ_INVALID_BITS,
READ_INVALID_HEADER
/* коды других ошибок */
};
```c
/* deserializer */
enum read_status {
READ_OK = 0,
READ_INVALID_SIGNATURE,
READ_INVALID_BITS,
READ_INVALID_HEADER
/* коды других ошибок */
};
enum read_status from_bmp( FILE* in, struct image* img );
enum read_status from_bmp( FILE* in, struct image* img );
/* serializer */
enum write_status {
WRITE_OK = 0,
WRITE_ERROR
/* коды других ошибок */
};
/* serializer */
enum write_status {
WRITE_OK = 0,
WRITE_ERROR
/* коды других ошибок */
};
enum write_status to_bmp( FILE* out, struct image const* img );
enum write_status to_bmp( FILE* out, struct image const* img );
```
Функции `from_bmp` и `to_bmp` принимают уже открытый файл, что позволяет
им работать с заранее открытыми файлами `stdin`, `stdout`, `stderr`.
Функции `from_bmp` и `to_bmp` не должны ни открывать, ни закрывать файлы. Для ошибок открытия/закрытия, возможно, вам захочется ввести отдельные типы перечислений.
Как только мы считали изображение во внутренний формат, мы должны забыть, из
какого формата оно было считано! Именно поэтому в `struct image` оставлен
только самый минимум деталей изображения (размеры), и никаких частей
bmp-заголовка.
Вам также потребуются функции, аналогичные `from_bmp` и `to_bmp`, которые будут
принимать имена файлов и заниматься корректным открытием (`fopen`) и
закрытием (`fclose`) файлов; на открытых файлах они могут запускать `from_bmp`
и `to_bmp`.
```
Имеет смысл разделять открытие/закрытие файлов и работу с ними. Уже
открытие и закрытие могут сопровождаться ошибками (см. `man fopen` и
`man fclose`) и хочется отделить обработку ошибок открытия/закрытия и
обработку ошибок чтения/записи.
Функции `from_bmp` и `to_bmp` принимают уже открытый файл, что позволяет
им работать с заранее открытыми файлами `stdin`, `stdout`, `stderr`.
Функции `from_bmp` и `to_bmp` не должны ни открывать, ни закрывать файлы.
Для ошибок открытия/закрытия, возможно, вам захочется ввести отдельные типы
перечислений.
Как только мы считали изображение во внутренний формат, мы должны забыть, из
какого формата оно было считано! Именно поэтому в `struct image` оставлен
только самый минимум деталей изображения (размеры), и никаких частей
bmp-заголовка.
Вам также потребуются функции, аналогичные `from_bmp` и `to_bmp`, которые
будут принимать имена файлов и заниматься корректным открытием (`fopen`) и
закрытием (`fclose`) файлов; на открытых файлах они могут запускать `from_bmp`
и `to_bmp`.
Имеет смысл разделять открытие/закрытие файлов и работу с ними. Уже
открытие и закрытие могут сопровождаться ошибками (см. `man fopen` и
`man fclose`) и хочется отделить обработку ошибок открытия/закрытия и
обработку ошибок чтения/записи.
## Часть 3: Трансформации
Каждая трансформация описывается в отдельном модуле. Эти модули знают про модуль, описывающий `struct image`, но ничего не знают про входные форматы.
Поэтому можно будет добавлять новые входные форматы не переписывая код для трансформаций. Без дополнительных усилий мы получим возможность, описав входной формат, сразу же поддержать все трансформации над ним.
Каждая трансформация описывается в отдельном модуле. Эти модули знают про
модуль, описывающий `struct image`, но ничего не знают про входные форматы.
Поэтому можно будет добавлять новые входные форматы не переписывая код для
трансформаций. Без дополнительных усилий мы получим возможность, описав входной
формат, сразу же поддержать все трансформации над ним.
Вам потребуется функция для поворота картинки в её внутреннем представлении:
......@@ -181,8 +202,8 @@ struct bmp_header
struct image rotate( struct image const source );
```
**ВНИМАНИЕ** `view-header` это программа для просмотра заголовков BMP файлов. Это не заготовка для решения! Можете скомпилировать её с помощью `make` и проверять заголовки на битность; в решении вам нужно поддерживать только 24-битные BMP файлы.
Вам может пригодиться [программа для отображения заголовков BMP файлов](https://gitlab.se.ifmo.ru/low-level-programming/bmp-header-viewer)
**ВНИМАНИЕ** это программа для просмотра заголовков BMP файлов. Это не заготовка для решения! Можете скомпилировать её с помощью `make` и проверять заголовки на битность; в решении вам нужно поддерживать только 24-битные BMP файлы.
## Часть 4: всё остальное
......@@ -196,9 +217,23 @@ struct bmp_header
# Задание
- Необходимо реализовать поворот изображения в формате BMP на 90 градусов по часовой стрелке. Имя изображения принимайте в аргументе командной строки.
- Архитектура приложения описана в предыдущем разделе.
- Необходимо реализовать поворот изображения в формате BMP на 90 градусов против
часовой стрелки. Формат использования такой:
```
./image-transformer <source-image> <transformed-image>
```
- Архитектура приложения описана в предыдущем разделе.
- Мы предоставляем вам `Makefile`, который нужно использовать. Заголовочные файлы ищутся в `include`.
Ваша программа будет тестироваться с помощью `clang` и проверяться
статическим анализатором `clang-tidy`. Флаги и формат его запуска можно
посмотреть
[тут](https://gitlab.se.ifmo.ru/low-level-programming/docker-c-test-machine/-/blob/master/to-docker-image/run-checks.sh).
Тесты запускаются в
[Docker-контейнере](https://gitlab.se.ifmo.ru/low-level-programming/docker-c-test-machine/),
вы можете использовать его для тестирования вашего кода локально.
Скрипт `run-tests` запускает тесты.
# Для самопроверки
......
echo "--------------------"
echo "Building tester"
echo "--------------------"
make -C tester
echo "Compiling and testing with sanitizers"
sanitizers=( '-fsanitize=leak -fsanitize=address'
'-fsanitize=memory -fsanitize-memory-track-origins=2'
'-fsanitize=undefined'
)
COMPARE_TEST_RESULTS=./tester/build/bmp-compare
for sanitizer in "${sanitizers[@]}"
do
echo "--------------------"
echo "Starting tests with the following sanitizer enabled: $sanitizer"
echo "--------------------"
make -C solution clean && make -C solution with="$sanitizer" || { echo "Unable to compile; see log" ; exit 1 ; }
# Here we launch tests
for test in `ls tester/tests/* -d` ;
do
fst="$test/input.bmp"
snd="$test/output.bmp"
expected="${snd/.bmp/_expected.bmp}"
./solution/build/image-transformer "$fst" "$snd"
echo -e "\ncomparing: $fst and $expected"
$COMPARE_TEST_RESULTS "$snd" "$expected" && echo -e "OK\n" || exit 1 ;
done
# tests end
echo ""
done
CC = clang
LINKER = clang
BUILDDIR = build
OBJDIR = obj
SRCDIR = src
TESTDIR = tests
INCLUDEDIR = include
CFLAGS = -c -std=c17 -I$(INCLUDEDIR) -ggdb -Wall -Werror -pedantic -Wno-attributes
LDFLAGS =
SOURCES = $(wildcard $(SRCDIR)/*.c) $(wildcard $(SRCDIR)/transform/*.c)
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
TARGET = image-transformer
ifneq ($(with), )
CFLAGS += $(with)
LDFLAGS += $(with)
endif
all: $(BUILDDIR)/$(TARGET)
$(BUILDDIR)/$(TARGET): $(OBJECTS) | $(BUILDDIR) $(BUILDDIR)/transform
$(LINKER) $(LDFLAGS) $(OBJECTS) -o $@
$(OBJECTS): $(OBJDIR)/%.o:$(SRCDIR)/%.c | $(OBJDIR) $(OBJDIR)/transform
$(CC) $(CFLAGS) -c $< -o $@
$(BUILDDIR) $(BUILDDIR)/transform $(OBJDIR) $(OBJDIR)/transform:
mkdir -p $@
clean:
rm -rf $(BUILDDIR) $(OBJDIR)
.PHONY: clean
-Iinclude/
-std=c17
-pedantic
-Wall
-Werror
-ggdb
-Wno-attributes
CC = clang
LINKER = clang
BUILDDIR = build
OBJDIR = obj
SRCDIR = src
TESTDIR = tests
INCLUDEDIR = include
CFLAGS = -c -std=c17 -I$(INCLUDEDIR) -ggdb -Wall -Werror -pedantic -Wno-attributes
LDFLAGS =
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
TARGET = bmp-compare
all: $(BUILDDIR)/$(TARGET)
$(BUILDDIR)/$(TARGET): $(OBJECTS) | $(BUILDDIR)
$(LINKER) $(LDFLAGS) $(OBJECTS) -o $@
$(OBJECTS): $(OBJDIR)/%.o:$(SRCDIR)/%.c | $(OBJDIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILDDIR) $(OBJDIR) :
mkdir -p $@
clean:
rm -rf $(BUILDDIR) $(OBJDIR)
.PHONY: clean
-Iinclude/
-std=c17
-pedantic
-Wall
-Werror
#pragma once
#include <stdbool.h>
#include "image.h"
enum bmp_compare_status {
BMP_CMP_EQUALS = 0,
BMP_CMP_DIFF,
BMP_CMP_FILE_ERROR,
BMP_CMP_DIMENSIONS_DIFFER,
BMP_CMP_INVALID_FORMAT, BMP_CMP_OUT_OF_MEMORY, BMP_CMP_INVALID_ARGUMENT
};
enum bmp_compare_status bmp_cmp(FILE* f1, FILE* f2);
static const char *const bmp_cmp_error_msg[] = {
[BMP_CMP_EQUALS] = "BMP files are similar",
[BMP_CMP_DIFF] = "Read from BMP file successful",
[BMP_CMP_FILE_ERROR] = "Invalid BMP file",
[BMP_CMP_DIMENSIONS_DIFFER] = "BMP files have different dimensions",
[BMP_CMP_INVALID_FORMAT] = "Invalid BMP file format",
[BMP_CMP_INVALID_ARGUMENT] =
"Internal error while reading BMP file: invalid argument"};
#pragma once
#include <stdbool.h>
#include <stdio.h>
#define CMP_BUFFER_SIZE (4096 * 2)
enum cmp_result {
CMP_EQ,
CMP_DIFF,
CMP_ERROR,
};
#pragma once
#define EXECUTABLE_NAME "image-transformer"
#pragma once
#include <stddef.h>
struct dimensions {
size_t x;
size_t y;
};
struct dimensions dimensions_reverse( const struct dimensions* dim );
#include <stdio.h>
#include "cmp.h"
enum cmp_result file_cmp(FILE *f1, FILE *f2, size_t sz);
#pragma once
#include <stddef.h>
#include <malloc.h>
#include <inttypes.h>
#include "dimensions.h"
struct pixel {
uint8_t components[3];
};
struct image {
struct dimensions size;
struct pixel* data;
};
struct image image_create( struct dimensions size );
void image_destroy( struct image* image );
#pragma once
#define PRI_SPECIFIER(e) (_Generic( (e), uint16_t : "%" PRIu16, uint32_t: "%" PRIu32, default: "NOT IMPLEMENTED" ))
_Noreturn void err( const char* msg, ... );
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include "bmp.h"
#include "common.h"
#include "file_cmp.h"
#include "image.h"
#include "io.h"
#define FOR_HEADER(FOR_FIELD) \
FOR_FIELD(uint8_t, bfType[2]) \
FOR_FIELD(uint32_t, bfileSize) \
FOR_FIELD(uint32_t, bfReserved) \
FOR_FIELD(uint32_t, bOffBits) \
FOR_FIELD(uint32_t, biSize) \
FOR_FIELD(uint32_t, biWidth) \
FOR_FIELD(uint32_t, biHeight) \
FOR_FIELD(uint16_t, biPlanes) \
FOR_FIELD(uint16_t, biBitCount) \
FOR_FIELD(uint32_t, biCompression) \
FOR_FIELD(uint32_t, biSizeImage) \
FOR_FIELD(uint32_t, biXPelsPerMeter) \
FOR_FIELD(uint32_t, biYPelsPerMeter) \
FOR_FIELD(uint32_t, biClrUsed) \
FOR_FIELD(uint32_t, biClrImportant)
#define DECLARE_FIELD(t, n) t n;
struct __attribute__((packed)) header {
FOR_HEADER(DECLARE_FIELD)
};
#undef FOR_HEADER
#undef DECLARE_FIELD
static size_t get_padding( size_t width ) {return width % 4;}
static bool header_is_correct(const struct header *header) {
if (header->bfType[0] != 'B' || header->bfType[1] != 'M')
return false;
if (header->biBitCount != 24)
return false;
return true;
}
enum bmp_compare_status bmp_cmp(FILE *f1, FILE *f2) {
struct header h1 = {0}, h2 = {0};
if (!fread(&h1, sizeof(struct header), 1, f1) ||
!header_is_correct(&h1))
return BMP_CMP_FILE_ERROR;
if (!fread(&h2, sizeof(struct header), 1, f2) ||
!header_is_correct(&h2))
return BMP_CMP_INVALID_FORMAT;
if (h1.biWidth != h2.biWidth || h1.biHeight != h2.biHeight)
return BMP_CMP_DIMENSIONS_DIFFER;
const size_t padding = get_padding(h1.biWidth);
for( size_t i = 0; i < h1.biHeight; i++ ) {
switch (file_cmp( f1, f2, h1.biWidth * 3 ) ) {
case CMP_DIFF: return BMP_CMP_DIFF;
case CMP_ERROR: return BMP_CMP_FILE_ERROR;
case CMP_EQ:
if (fseek(f1, padding, SEEK_CUR) != 0 ||
fseek(f2, padding, SEEK_CUR) != 0)
return BMP_CMP_FILE_ERROR;
break;
default: err("Implementation error"); break;
}
}
return BMP_CMP_EQUALS;
}
#include <stdint.h>
#include <string.h>
#include "cmp.h"
#include "file_cmp.h"
static size_t min(size_t x, size_t y) {
if (x < y)
return x;
return y;
}
enum cmp_interm_result { CMP_INT_EQ, CMP_INT_DIFF, CMP_INT_ERROR, CMP_INT_UNDEF };
static enum cmp_interm_result cmp_chunk(uint8_t buf1[], uint8_t buf2[],
size_t buf_size, size_t read1, size_t read2) {
if (read1 == read2) {
if (memcmp(buf1, buf2, read1) != 0)
return CMP_INT_DIFF;
if (read1 == buf_size)
return CMP_INT_UNDEF;
else
return CMP_INT_EQ;
} else {
return CMP_INT_DIFF;
}
}
enum cmp_result file_cmp(FILE *f1, FILE *f2, size_t sz) {
uint8_t buffer1[CMP_BUFFER_SIZE];
uint8_t buffer2[CMP_BUFFER_SIZE];
while (sz) {
const size_t chunk_size = min(CMP_BUFFER_SIZE, sz);
const size_t read1 = fread(buffer1, 1, chunk_size, f1);
const size_t read2 = fread(buffer2, 1, chunk_size, f2);
const enum cmp_interm_result result =
cmp_chunk(buffer1, buffer2, CMP_BUFFER_SIZE, read1, read2);
switch (result) {
case CMP_INT_EQ: return CMP_EQ;
case CMP_INT_DIFF: return CMP_DIFF;
case CMP_INT_ERROR: return CMP_ERROR;
case CMP_INT_UNDEF:
sz -= CMP_BUFFER_SIZE;
continue;
}
}
return CMP_EQ;
}
#include <stdbool.h>
#include <stdio.h>
#include "bmp.h"
#include "common.h"
#include "io.h"
void usage() {
fprintf(stderr,
"Usage: ./" EXECUTABLE_NAME " BMP_FILE_NAME1 BMP_FILE_NAME2\n");
}
int main(int argc, char **argv) {
if (argc != 3)
usage();
// error handling should be here
FILE *f1 = fopen(argv[1], "rb");
if (!f1)
err("Bad first input file\n");
FILE *f2 = fopen(argv[2], "rb");
if (!f2) {
fclose(f1);
err("Bad second input file\n");
}
const enum bmp_compare_status status = bmp_cmp(f1, f2);
fclose(f1);
fclose(f2);
if (status == BMP_CMP_EQUALS)
return 0;
fprintf(stderr, "%s\n", bmp_cmp_error_msg[status]);
return status;
}
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
_Noreturn void err( const char* msg, ... ) {
va_list args;
va_start (args, msg);
// There is a bug in clang-tidy that makes it consider args as uninitialized
// NOLINT helps supress this message
// See: https://bugs.llvm.org/show_bug.cgi?id=41311
vfprintf(stderr, msg, args); // NOLINT
va_end (args);
exit(1);
}
*/output.bmp
tester/tests/1/input.bmp

4.09 MB

tester/tests/1/output_expected.bmp

4.09 MB

Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment