diff --git a/forms/interface.ui b/forms/interface.ui index 9b4054ba50695ea12a25648cf8b98456327dc954..cda944d6682c1c65e02995a2a333163c9ba2c470 100644 --- a/forms/interface.ui +++ b/forms/interface.ui @@ -138,6 +138,9 @@ <property name="minimum"> <number>1</number> </property> + <property name="maximum"> + <number>100</number> + </property> <property name="value"> <number>20</number> </property> @@ -621,7 +624,10 @@ pattern recorded :</string> <property name="title"> <string>Edit</string> </property> + <addaction name="separator"/> <addaction name="actionCharger_depuis_une_image"/> + <addaction name="action_save_image"/> + <addaction name="action_save_gif"/> <addaction name="action_save_struct"/> </widget> <widget class="QMenu" name="menuA_propos"> @@ -682,6 +688,16 @@ pattern recorded :</string> <string>Play Snake</string> </property> </action> + <action name="action_save_image"> + <property name="text"> + <string>Save as an image...</string> + </property> + </action> + <action name="action_save_gif"> + <property name="text"> + <string>Save as an animated GIF...</string> + </property> + </action> </widget> <customwidgets> <customwidget> diff --git a/include/gridview.hpp b/include/gridview.hpp index a43036df39716b96e67af3e193dc1dcb12b56eac..eaf451ef3074519c065feaebf037e9a7ed7ede6c 100644 --- a/include/gridview.hpp +++ b/include/gridview.hpp @@ -139,6 +139,9 @@ public: //! \brief Annule la dernière opération (effectue un Ctrl-Z). void undo(); + //! \brief Retourne une QImage représentant la grille actuelle. + const QImage &grid_image() const; + signals: //! \brief Signal émis quand le zoom change. //! \param cell_size la nouvelle taille à l'écran en pixels d'une cellule @@ -159,8 +162,6 @@ private: void click_on(Coord coord); void update_current_mouse_pos(Coord coord); - const QImage &grid_image() const; - private slots: void handle_rubberband(QRect, QPointF, QPointF); diff --git a/include/history.h b/include/history.h index dcc555bea7b66369be9b0e83a30553ee5c13e431..11dcd6f3a35b3a0b209d62cb7d267d858aed4c7d 100644 --- a/include/history.h +++ b/include/history.h @@ -48,6 +48,18 @@ public: //! \return Retourne le nombre de lignes de la Grille unsigned int get_nbMax()const{return nbMax;} + //! \brief Retourne le nombre de grilles contenues dans l'historique. + unsigned int size() const + { return tab.size(); } + + //! \brief Accède à la grille d'indice i. + Grid at(unsigned int i) const + { + if (i >= size()) + throw HistoryException("The stack is empty."); + return tab[i]; + } + //! \brief Ajoute une grille dans l'historique void pushGrid(const Grid& g); diff --git a/include/interface.hpp b/include/interface.hpp index 76375edf61b4e2c62c3285b9cc7b32009182e142..42a9c0e451ebcd140868a6295fab1a197d42dc2b 100644 --- a/include/interface.hpp +++ b/include/interface.hpp @@ -54,6 +54,12 @@ private slots: //! \returns Le nom du fichier choisi QString afficher_interface_sauvegarde_structure(); + //! \brief Sauvegarde la grille actuelle en tant qu'image. + void save_as_image(); + + //! \brief Sauvegarde l'historique d'exécution en tant que GIF animé. + void save_as_gif(); + void on_nbrStateComboBox_currentTextChanged(const QString &arg1); //! \brief Créer une nouvelle grille ayant des états aléatoire diff --git a/include/simulation.hpp b/include/simulation.hpp index 2176ca4e511495cb09d5657a2b0ee36c226bfaf0..43d391cde38e87aa5d58752cd9d5e4862e41be63 100644 --- a/include/simulation.hpp +++ b/include/simulation.hpp @@ -61,7 +61,9 @@ public: //! Vide également l'historique void setHistorySize(unsigned int size) {hist = History(size);} - + //! \brief Retourne une référence constante vers l'History actuel. + const History& getHistory() const + { return hist; } //! \brief Définit la grille //! diff --git a/src/gif/README b/src/gif/README new file mode 100644 index 0000000000000000000000000000000000000000..3c1d2b763cc6af566c4e2349020e8284b43e1c00 --- /dev/null +++ b/src/gif/README @@ -0,0 +1,142 @@ +Source: https://github.com/lecram/gifenc + +GIF encoder +=========== + +This is a small C library that can be used to create GIF animations. + +Features +-------- + + * user-defined palette of any depth from 1 up to 8 + * each frame has its own (user-specified) delay time + * flexible looping options: no loop, N repetitions, infinite loop + * GIF size optimization: only stores frame differences + * memory efficient: saves frames to file as soon as possible + * small and portable: less than 300 lines of C99 + * public domain + + +Limitations +----------- + + * no frame-local palettes (incompatible with size optimization) + * no interlacing (bad for compression, useless for animations) + + +Documentation +------------- + +There are only three functions declared in "gifenc.h": ge_new_gif(), +ge_add_frame() and ge_close_gif(). + +The ge_new_gif() function receives GIF global options and returns a ge_GIF +handler: + + ge_GIF *ge_new_gif( + const char *fname, /* GIF file name */ + uint16_t width, uint16_t height, /* frame size */ + uint8_t *palette, int depth, /* color table */ + int loop /* looping information */ + ); + +The `palette` parameter must point to an array of color data. Each entry is a +24-bits RGB color, stored as three contiguous bytes: the first is the red value +(0-255), then green, then blue. Entries are stored in a contiguous byte array. + +The `depth` parameter specifies how many colors are present in the given +palette. The number of color entries must be 2 ^ depth, where 1 <= depth <= 8. + +Example `palette` and `depth` values: + + uint8_t palette[] = { + 0x00, 0x00, 0x00, /* entry 0: black */ + 0xFF, 0xFF, 0xFF, /* entry 1: white */ + 0xFF, 0x00, 0x00, /* entry 2: red */ + 0x00, 0x00, 0xFF, /* entry 3: blue */ + }; + int depth = 2; /* palette has 1 << 2 (i.e. 4) entries */ + +If `palette` is NULL, entries are taken from a default table of 256 colors. If +`depth` < 8, the default table will be truncated to the appropriate size. The +default table is composed of the 16 standard VGA colors, plus the 216 web-safe +colors (all combinations of RGB with only 6 valid values per channel), plus 24 +grey colors equally spaced between black and white, excluding both. + +If `depth` < 0 and `palette` is not NULL, then the default table with 2 ^ -depth +colors is used and it is stored in the array at the `palette` address. + +If the `loop` parameter is zero, the resulting GIF will loop forever. If it is a +positive number, the animation will be played that number of times. If `loop` +is negative, no looping information is stored in the GIF file (for most GIF +viewers, this is equivalent to `loop` == 1, i.e., "play once"). + +The ge_add_frame() function reads pixel data from a buffer and saves the +resulting frame to the file associated with the given ge_GIF handler: + + void ge_add_frame(ge_GIF *gif, uint16_t delay); + +The `delay` parameter specifies how long the frame will be shown, in hundreths +of a second. For example, `delay` == 100 means "show this frame for one second" +and `delay` == 25 means "show this frame for a quarter of a second". Note that +short delays may not be supported by some GIF viewers: it's recommended to keep +a minimum of `delay` == 6. If `delay` == 0, no delay information will be stored +for the frame. This can be used when creating still (single-frame) GIF images. + +Pixel data is read from `gif->frame`, which points to a memory block like this: + + uint8_t _frame_[gif->width * gif->height]; + +Note that the address of `gif->frame` changes between calls to ge_add_frame() +(*). For this reason, each frame must be written in its entirety to the current +address, even if one only wants to change a few pixels from the last frame. The +encoder will automatically detect the difference between two consecutive frames +in order to minimize the size of the output. + +Each byte in the frame buffer represents a pixel. The value of each pixel is an +index to a palette entry. For instance, given the example palette above, we can +create a frame displaying a red-on-black "F" letter like this: + + uint8_t pixels[] = { + 2, 2, 2, 2, + 2, 0, 0, 0, + 2, 0, 0, 0, + 2, 2, 2, 0, + 2, 0, 0, 0, + 2, 0, 0, 0, + 2, 0, 0, 0 + }; + ge_GIF *gif = ge_new_gif("F.gif", 4, 7, palette, depth, -1); + memcpy(gif->frame, pixels, sizeof(pixels)); + ge_add_frame(gif, 0); + ge_close_gif(gif); + +The function ge_close_gif() finishes writting GIF data to the file associated +with the given ge_GIF handler and does memory clean-up. This function must be +called once after all desired frames have been added, in order to correctly save +the GIF file. After calling this function, the ge_GIF handler cannot be used +anymore. + + void ge_close_gif(ge_GIF* gif); + +(*) The encoder keeps two frame buffers internally, in order to implement the +size optimization. The address of `gif->frame` alternates between those two +buffers after each call to ge_add_frame(). + + +Example +------- + +See the file "example.c". It can be tested like this: + +$ cc -o example gifenc.c example.c +$ ./example + +That should create an animated GIF named "example.gif". + + +Copying +------- + +All of the source code and documentation for gifenc is released into the +public domain and provided without warranty of any kind. diff --git a/src/gif/gifenc.c b/src/gif/gifenc.c new file mode 100644 index 0000000000000000000000000000000000000000..5be9ffe354eb62627c24cbaafef3dffa09280781 --- /dev/null +++ b/src/gif/gifenc.c @@ -0,0 +1,316 @@ +#include "gifenc.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#ifdef _WIN32 +#include <io.h> +#else +#include <unistd.h> +#endif + +/* helper to write a little-endian 16-bit number portably */ +#define write_num(fd, n) write((fd), (uint8_t []) {(n) & 0xFF, (n) >> 8}, 2) + +static uint8_t vga[0x30] = { + 0x00, 0x00, 0x00, + 0xAA, 0x00, 0x00, + 0x00, 0xAA, 0x00, + 0xAA, 0x55, 0x00, + 0x00, 0x00, 0xAA, + 0xAA, 0x00, 0xAA, + 0x00, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, + 0x55, 0x55, 0x55, + 0xFF, 0x55, 0x55, + 0x55, 0xFF, 0x55, + 0xFF, 0xFF, 0x55, + 0x55, 0x55, 0xFF, + 0xFF, 0x55, 0xFF, + 0x55, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, +}; + +struct Node { + uint16_t key; + struct Node *children[]; +}; +typedef struct Node Node; + +static Node * +new_node(uint16_t key, int degree) +{ + Node *node = calloc(1, sizeof(*node) + degree * sizeof(Node *)); + if (node) + node->key = key; + return node; +} + +static Node * +new_trie(int degree, int *nkeys) +{ + Node *root = new_node(0, degree); + /* Create nodes for single pixels. */ + for (*nkeys = 0; *nkeys < degree; (*nkeys)++) + root->children[*nkeys] = new_node(*nkeys, degree); + *nkeys += 2; /* skip clear code and stop code */ + return root; +} + +static void +del_trie(Node *root, int degree) +{ + if (!root) + return; + for (int i = 0; i < degree; i++) + del_trie(root->children[i], degree); + free(root); +} + +#define write_and_store(s, dst, fd, src, n) \ +do { \ + write(fd, src, n); \ + if (s) { \ + memcpy(dst, src, n); \ + dst += n; \ + } \ +} while (0); + +static void put_loop(ge_GIF *gif, uint16_t loop); + +ge_GIF * +ge_new_gif( + const char *fname, uint16_t width, uint16_t height, + uint8_t *palette, int depth, int loop +) +{ + int i, r, g, b, v; + int store_gct, custom_gct; + ge_GIF *gif = calloc(1, sizeof(*gif) + 2*width*height); + if (!gif) + goto no_gif; + gif->w = width; gif->h = height; + gif->frame = (uint8_t *) &gif[1]; + gif->back = &gif->frame[width*height]; +#ifdef _WIN32 + gif->fd = creat(fname, S_IWRITE); +#else + gif->fd = creat(fname, 0666); +#endif + if (gif->fd == -1) + goto no_fd; +#ifdef _WIN32 + setmode(gif->fd, O_BINARY); +#endif + write(gif->fd, "GIF89a", 6); + write_num(gif->fd, width); + write_num(gif->fd, height); + store_gct = custom_gct = 0; + if (palette) { + if (depth < 0) + store_gct = 1; + else + custom_gct = 1; + } + if (depth < 0) + depth = -depth; + gif->depth = depth > 1 ? depth : 2; + write(gif->fd, (uint8_t []) {0xF0 | (depth-1), 0x00, 0x00}, 3); + if (custom_gct) { + write(gif->fd, palette, 3 << depth); + } else if (depth <= 4) { + write_and_store(store_gct, palette, gif->fd, vga, 3 << depth); + } else { + write_and_store(store_gct, palette, gif->fd, vga, sizeof(vga)); + i = 0x10; + for (r = 0; r < 6; r++) { + for (g = 0; g < 6; g++) { + for (b = 0; b < 6; b++) { + write_and_store(store_gct, palette, gif->fd, + ((uint8_t []) {r*51, g*51, b*51}), 3 + ); + if (++i == 1 << depth) + goto done_gct; + } + } + } + for (i = 1; i <= 24; i++) { + v = i * 0xFF / 25; + write_and_store(store_gct, palette, gif->fd, + ((uint8_t []) {v, v, v}), 3 + ); + } + } +done_gct: + if (loop >= 0 && loop <= 0xFFFF) + put_loop(gif, (uint16_t) loop); + return gif; +no_fd: + free(gif); +no_gif: + return NULL; +} + +static void +put_loop(ge_GIF *gif, uint16_t loop) +{ + write(gif->fd, (uint8_t []) {'!', 0xFF, 0x0B}, 3); + write(gif->fd, "NETSCAPE2.0", 11); + write(gif->fd, (uint8_t []) {0x03, 0x01}, 2); + write_num(gif->fd, loop); + write(gif->fd, "\0", 1); +} + +/* Add packed key to buffer, updating offset and partial. + * gif->offset holds position to put next *bit* + * gif->partial holds bits to include in next byte */ +static void +put_key(ge_GIF *gif, uint16_t key, int key_size) +{ + int byte_offset, bit_offset, bits_to_write; + byte_offset = gif->offset / 8; + bit_offset = gif->offset % 8; + gif->partial |= ((uint32_t) key) << bit_offset; + bits_to_write = bit_offset + key_size; + while (bits_to_write >= 8) { + gif->buffer[byte_offset++] = gif->partial & 0xFF; + if (byte_offset == 0xFF) { + write(gif->fd, "\xFF", 1); + write(gif->fd, gif->buffer, 0xFF); + byte_offset = 0; + } + gif->partial >>= 8; + bits_to_write -= 8; + } + gif->offset = (gif->offset + key_size) % (0xFF * 8); +} + +static void +end_key(ge_GIF *gif) +{ + int byte_offset; + byte_offset = gif->offset / 8; + if (gif->offset % 8) + gif->buffer[byte_offset++] = gif->partial & 0xFF; + if (byte_offset) { + write(gif->fd, (uint8_t []) {byte_offset}, 1); + write(gif->fd, gif->buffer, byte_offset); + } + write(gif->fd, "\0", 1); + gif->offset = gif->partial = 0; +} + +static void +put_image(ge_GIF *gif, uint16_t w, uint16_t h, uint16_t x, uint16_t y) +{ + int nkeys, key_size, i, j; + Node *node, *child, *root; + int degree = 1 << gif->depth; + + write(gif->fd, ",", 1); + write_num(gif->fd, x); + write_num(gif->fd, y); + write_num(gif->fd, w); + write_num(gif->fd, h); + write(gif->fd, (uint8_t []) {0x00, gif->depth}, 2); + root = node = new_trie(degree, &nkeys); + key_size = gif->depth + 1; + put_key(gif, degree, key_size); /* clear code */ + for (i = y; i < y+h; i++) { + for (j = x; j < x+w; j++) { + uint8_t pixel = gif->frame[i*gif->w+j] & (degree - 1); + child = node->children[pixel]; + if (child) { + node = child; + } else { + put_key(gif, node->key, key_size); + if (nkeys < 0x1000) { + if (nkeys == (1 << key_size)) + key_size++; + node->children[pixel] = new_node(nkeys++, degree); + } else { + put_key(gif, degree, key_size); /* clear code */ + del_trie(root, degree); + root = node = new_trie(degree, &nkeys); + key_size = gif->depth + 1; + } + node = root->children[pixel]; + } + } + } + put_key(gif, node->key, key_size); + put_key(gif, degree + 1, key_size); /* stop code */ + end_key(gif); + del_trie(root, degree); +} + +static int +get_bbox(ge_GIF *gif, uint16_t *w, uint16_t *h, uint16_t *x, uint16_t *y) +{ + int i, j, k; + int left, right, top, bottom; + left = gif->w; right = 0; + top = gif->h; bottom = 0; + k = 0; + for (i = 0; i < gif->h; i++) { + for (j = 0; j < gif->w; j++, k++) { + if (gif->frame[k] != gif->back[k]) { + if (j < left) left = j; + if (j > right) right = j; + if (i < top) top = i; + if (i > bottom) bottom = i; + } + } + } + if (left != gif->w && top != gif->h) { + *x = left; *y = top; + *w = right - left + 1; + *h = bottom - top + 1; + return 1; + } else { + return 0; + } +} + +static void +set_delay(ge_GIF *gif, uint16_t d) +{ + write(gif->fd, (uint8_t []) {'!', 0xF9, 0x04, 0x04}, 4); + write_num(gif->fd, d); + write(gif->fd, "\0\0", 2); +} + +void +ge_add_frame(ge_GIF *gif, uint16_t delay) +{ + uint16_t w, h, x, y; + uint8_t *tmp; + + if (delay) + set_delay(gif, delay); + if (gif->nframes == 0) { + w = gif->w; + h = gif->h; + x = y = 0; + } else if (!get_bbox(gif, &w, &h, &x, &y)) { + /* image's not changed; save one pixel just to add delay */ + w = h = 1; + x = y = 0; + } + put_image(gif, w, h, x, y); + gif->nframes++; + tmp = gif->back; + gif->back = gif->frame; + gif->frame = tmp; +} + +void +ge_close_gif(ge_GIF* gif) +{ + write(gif->fd, ";", 1); + close(gif->fd); + free(gif); +} diff --git a/src/gif/gifenc.h b/src/gif/gifenc.h new file mode 100644 index 0000000000000000000000000000000000000000..9b63c7eaf9fb16e2d4db8884f3031b1c51d45218 --- /dev/null +++ b/src/gif/gifenc.h @@ -0,0 +1,31 @@ +#ifndef GIFENC_H +#define GIFENC_H + +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ge_GIF { + uint16_t w, h; + int depth; + int fd; + int offset; + int nframes; + uint8_t *frame, *back; + uint32_t partial; + uint8_t buffer[0xFF]; +} ge_GIF; + +ge_GIF *ge_new_gif( + const char *fname, uint16_t width, uint16_t height, + uint8_t *palette, int depth, int loop +); +void ge_add_frame(ge_GIF *gif, uint16_t delay); +void ge_close_gif(ge_GIF* gif); + +#ifdef __cplusplus +} +#endif +#endif /* GIFENC_H */ diff --git a/src/interface.cpp b/src/interface.cpp index 30fe9a488c1c4cf6a44b656ab59d6c4e930beb19..59a484df4a64abaeb6baf94b98cbc8d9445b92fe 100644 --- a/src/interface.cpp +++ b/src/interface.cpp @@ -1,4 +1,4 @@ -#include "interface.hpp" +#include "interface.hpp" #include "ui_interface.h" #include "savingdialog.hpp" @@ -13,6 +13,8 @@ #include "neighborhoodDialog.hpp" #include "colorlabel.h" +#include "gif/gifenc.h" + #include <QtGlobal> #include <QJsonArray> #include <QDate> @@ -78,6 +80,8 @@ MainWindow::MainWindow(QWidget *parent) { load_from_image(); }); + connect(ui->action_save_image, &QAction::triggered, this, &MainWindow::save_as_image); + connect(ui->action_save_gif, &QAction::triggered, this, &MainWindow::save_as_gif); connect(ui->border_combo, QOverload<int>::of(&QComboBox::activated), this, &MainWindow::load_boundary_policy); ui->struct_library->update_cell_pixel_size(ui->grid_view->cell_screen_size()); @@ -192,6 +196,62 @@ QString MainWindow::afficher_interface_sauvegarde_structure() return filename; } +void MainWindow::save_as_image() +{ + QString filename = QFileDialog::getSaveFileName(this, tr("Save image"),"",tr("PNG (*.png);;JPG (*.jpg);;BMP (*.bmp)")); + if (filename.isEmpty()) + return; + + QImage img = ui->grid_view->grid_image(); + if (!img.save(filename)) + QMessageBox::warning(this, "Error", "Could not save file"); +} + +void MainWindow::save_as_gif() +{ + const unsigned width = ui->grid_view->get_grid().get_col(); + const unsigned height = ui->grid_view->get_grid().get_rows(); + const unsigned delay = 100/ui->simSpeedSlider->value(); // hundredths of second + const Alphabet& alph = ui->grid_view->alphabet(); + unsigned state_count = alph.taille(); + unsigned palette_depth = 0; + while (state_count >>= 1) ++palette_depth; // log2 + + QString filename = QFileDialog::getSaveFileName(this, tr("Save GIF"),"",tr("GIF (*.gif)")); + if (filename.isEmpty()) + return; + + std::vector<uint8_t> palette; + for (unsigned i = 0; i < alph.taille(); ++i) + { + palette.push_back(alph.getState(i).getColor().getRed()); + palette.push_back(alph.getState(i).getColor().getGreen()); + palette.push_back(alph.getState(i).getColor().getBlue()); + } + + ge_GIF* gif = ge_new_gif(filename.toStdString().c_str(), width, height, palette.data(), palette_depth, 0); + + const History& hist = simulation.getHistory(); + for (unsigned k = 0; k < hist.size(); ++k) + { + Grid grid = hist.at(k); + // L'historique contient une grid de taille différente, on l'ignore + if (grid.get_col() != width || grid.get_rows() != height) + continue; + + for(int i = 0; i < (int)height; ++i) + { + for(int j = 0; j < (int)width; ++j) + { + gif->frame[i*width + j] = grid.get_state({j,i}); + } + } + ge_add_frame(gif, delay); + } + + ge_close_gif(gif); +} + void MainWindow::on_validateGridDim_clicked() { diff --git a/src/src.pro b/src/src.pro index bef270a92c81ba2833f07a4861ad9d0765e9bbda..ad38b464b7998ad2bd5e0b796af64a877b5d5f6f 100644 --- a/src/src.pro +++ b/src/src.pro @@ -40,6 +40,7 @@ SOURCES += \ interface.cpp \ grid.cpp \ history.cpp \ + gif/gifenc.c \ neighborhood_rules/vonNeumannNeighborhoodRule.cpp \ structurewriter.cpp \ structurelibraryview.cpp \