essence-os/apps/image_editor.cpp

932 lines
34 KiB
C++

// This file is part of the Essence operating system.
// It is released under the terms of the MIT license -- see LICENSE.md.
// Written by: nakst.
// TODO Saving.
// TODO Don't use an EsPaintTarget for the bitmap?
// TODO Show brush preview.
// TODO Other tools: text, selection.
// TODO Resize and crop image.
// TODO Clipboard.
// TODO Clearing textbox undo from EsTextboxInsert?
// TODO Handling out of memory.
// TODO Color palette.
// TODO More brushes?
// TODO Grid.
// TODO Zoom and pan with EsCanvasPane.
// TODO Status bar.
// TODO Clearing undo if too much memory is used.
#define ES_INSTANCE_TYPE Instance
#include <essence.h>
#include <shared/array.cpp>
#include <shared/strings.cpp>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STBI_WRITE_NO_STDIO
#define STBIW_MEMMOVE EsCRTmemmove
#define STBIW_MALLOC(sz) EsCRTmalloc(sz)
#define STBIW_REALLOC(p,newsz) EsCRTrealloc(p,newsz)
#define STBIW_FREE(p) EsCRTfree(p)
#define STBIW_ASSERT EsAssert
#include <shared/stb_image_write.h>
#define TILE_SIZE (128)
struct Tile {
size_t referenceCount;
uint64_t ownerID;
uint32_t bits[TILE_SIZE * TILE_SIZE];
};
struct Image {
Tile **tiles;
size_t tileCountX, tileCountY;
uint64_t id;
uint32_t width, height;
};
struct Instance : EsInstance {
EsElement *canvas;
EsColorWell *colorWell;
EsTextbox *brushSize;
EsPanel *toolPanel;
EsButton *toolDropdown;
EsPaintTarget *bitmap;
uint32_t bitmapWidth, bitmapHeight;
Image image;
uint64_t nextImageID;
EsCommand commandBrush;
EsCommand commandFill;
EsCommand commandRectangle;
EsCommand commandSelect;
EsCommand commandText;
// Data while drawing:
EsRectangle modifiedBounds;
float previousPointX, previousPointY;
bool dragged;
};
const EsInstanceClassEditorSettings editorSettings = {
INTERFACE_STRING(ImageEditorNewFileName),
INTERFACE_STRING(ImageEditorNewDocument),
ES_ICON_IMAGE_X_GENERIC,
};
const EsStyle styleImageMenuTable = {
.inherit = ES_STYLE_PANEL_FORM_TABLE,
.metrics = {
.mask = ES_THEME_METRICS_INSETS,
.insets = ES_RECT_4(20, 20, 5, 8),
},
};
Image ImageFork(Instance *instance, Image image, uint32_t newWidth = 0, uint32_t newHeight = 0) {
Image copy = image;
copy.width = newWidth ?: image.width;
copy.height = newHeight ?: image.height;
copy.tileCountX = newWidth ? (newWidth + TILE_SIZE - 1) / TILE_SIZE : image.tileCountX;
copy.tileCountY = newHeight ? (newHeight + TILE_SIZE - 1) / TILE_SIZE : image.tileCountY;
copy.id = instance->nextImageID++;
copy.tiles = (Tile **) EsHeapAllocate(copy.tileCountX * copy.tileCountY * sizeof(Tile *), true);
for (uintptr_t y = 0; y < copy.tileCountY; y++) {
for (uintptr_t x = 0; x < copy.tileCountX; x++) {
uintptr_t source = y * image.tileCountX + x;
uintptr_t destination = y * copy.tileCountX + x;
if (y < image.tileCountY && x < image.tileCountX && image.tiles[source]) {
image.tiles[source]->referenceCount++;
copy.tiles[destination] = image.tiles[source];
}
}
}
return copy;
}
void ImageDelete(Image image) {
for (uintptr_t i = 0; i < image.tileCountX * image.tileCountY; i++) {
image.tiles[i]->referenceCount--;
if (!image.tiles[i]->referenceCount) {
// EsPrint("free tile %d, %d from image %d\n", i % image.tileCountX, i / image.tileCountX, image.tiles[i]->ownerID);
EsHeapFree(image.tiles[i]);
}
}
EsHeapFree(image.tiles);
}
void ImageCopyToPaintTarget(Instance *instance, const Image *image) {
uint32_t *bits;
size_t width, height, stride;
EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
for (int32_t i = 0; i < (int32_t) image->tileCountY; i++) {
for (int32_t j = 0; j < (int32_t) image->tileCountX; j++) {
Tile *tile = image->tiles[i * image->tileCountX + j];
int32_t copyWidth = TILE_SIZE, copyHeight = TILE_SIZE;
if (j * TILE_SIZE + copyWidth > (int32_t) width) {
copyWidth = width - j * TILE_SIZE;
}
if (i * TILE_SIZE + copyHeight > (int32_t) height) {
copyHeight = height - i * TILE_SIZE;
}
if (tile) {
for (int32_t y = 0; y < copyHeight; y++) {
for (int32_t x = 0; x < copyWidth; x++) {
bits[stride / 4 * (y + i * TILE_SIZE) + (x + j * TILE_SIZE)] = tile->bits[y * TILE_SIZE + x];
}
}
} else {
for (int32_t y = 0; y < copyHeight; y++) {
for (int32_t x = 0; x < copyWidth; x++) {
bits[stride / 4 * (y + i * TILE_SIZE) + (x + j * TILE_SIZE)] = 0;
}
}
}
}
}
EsPaintTargetEndDirectAccess(instance->bitmap);
}
Tile *ImageUpdateTile(Image *image, uint32_t x, uint32_t y, bool copyOldBits) {
EsAssert(x < image->tileCountX && y < image->tileCountY);
Tile **tileReference = image->tiles + y * image->tileCountX + x;
Tile *tile = *tileReference;
if (!tile || tile->ownerID != image->id) {
if (tile && tile->referenceCount == 1) {
tile->ownerID = image->id;
// EsPrint("reuse tile %d, %d for image %d\n", x, y, image->id);
} else {
Tile *old = tile;
if (old) old->referenceCount--;
*tileReference = tile = (Tile *) EsHeapAllocate(sizeof(Tile), false);
tile->referenceCount = 1;
tile->ownerID = image->id;
if (copyOldBits && old) {
EsMemoryCopy(tile->bits, old->bits, sizeof(old->bits));
}
// EsPrint("allocate new tile %d, %d for image %d\n", x, y, image->id);
}
}
return tile;
}
void ImageCopyFromPaintTarget(Instance *instance, Image *image, EsRectangle modifiedBounds) {
uint32_t *bits;
size_t width, height, stride;
EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
modifiedBounds = EsRectangleIntersection(modifiedBounds, ES_RECT_4(0, width, 0, height));
for (int32_t i = modifiedBounds.t / TILE_SIZE; i <= modifiedBounds.b / TILE_SIZE; i++) {
for (int32_t j = modifiedBounds.l / TILE_SIZE; j <= modifiedBounds.r / TILE_SIZE; j++) {
if ((uint32_t) j >= image->tileCountX || (uint32_t) i >= image->tileCountY) {
continue;
}
Tile *tile = ImageUpdateTile(image, j, i, false);
int32_t copyWidth = TILE_SIZE, copyHeight = TILE_SIZE;
if (j * TILE_SIZE + copyWidth > (int32_t) width) {
copyWidth = width - j * TILE_SIZE;
}
if (i * TILE_SIZE + copyHeight > (int32_t) height) {
copyHeight = height - i * TILE_SIZE;
}
for (int32_t y = 0; y < copyHeight; y++) {
for (int32_t x = 0; x < copyWidth; x++) {
tile->bits[y * TILE_SIZE + x] = bits[stride / 4 * (y + i * TILE_SIZE) + (x + j * TILE_SIZE)];
}
}
}
}
EsPaintTargetEndDirectAccess(instance->bitmap);
}
void ImageUndoMessage(const void *item, EsUndoManager *manager, EsMessage *message) {
const Image *image = (const Image *) item;
Instance *instance = EsUndoGetInstance(manager);
if (message->type == ES_MSG_UNDO_INVOKE) {
EsUndoPush(manager, ImageUndoMessage, &instance->image, sizeof(Image));
instance->image = *image;
if (instance->bitmapWidth != image->width || instance->bitmapHeight != image->height) {
instance->bitmapWidth = image->width;
instance->bitmapHeight = image->height;
EsPaintTargetDestroy(instance->bitmap);
instance->bitmap = EsPaintTargetCreate(instance->bitmapWidth, instance->bitmapHeight, false);
EsElementRelayout(EsElementGetLayoutParent(instance->canvas));
}
ImageCopyToPaintTarget(instance, image);
EsElementRepaint(instance->canvas);
} else if (message->type == ES_MSG_UNDO_CANCEL) {
ImageDelete(*image);
}
}
int BrushSizeMessage(EsElement *element, EsMessage *message) {
EsTextbox *textbox = (EsTextbox *) element;
if (message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA) {
double oldValue = EsTextboxGetContentsAsDouble(textbox);
double newValue = oldValue + message->numberDragDelta.delta * (message->numberDragDelta.fast ? 1 : 0.1);
if (newValue < 1) {
newValue = 1;
} else if (newValue > 200) {
newValue = 200;
}
char result[64];
size_t resultBytes = EsStringFormat(result, sizeof(result), "%d.%d", (int) newValue, (int) (newValue * 10) % 10);
EsTextboxSelectAll(textbox);
EsTextboxInsert(textbox, result, resultBytes);
}
return 0;
}
EsRectangle DrawFill(Instance *instance, EsPoint point) {
uint32_t color = 0xFF000000 | EsColorWellGetRGB(instance->colorWell);
EsRectangle modifiedBounds = ES_RECT_4(point.x, point.x, point.y, point.y);
if ((uint32_t) point.x >= instance->bitmapWidth || (uint32_t) point.y >= instance->bitmapHeight) {
return {};
}
uint32_t *bits;
size_t width, height, stride;
EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
stride /= 4;
Array<EsPoint> pointsToVisit = {};
uint32_t replaceColor = bits[point.y * stride + point.x];
if (replaceColor != color) pointsToVisit.Add(point);
while (pointsToVisit.Length()) {
EsPoint startPoint = pointsToVisit.Pop();
if (startPoint.y < modifiedBounds.t) {
modifiedBounds.t = startPoint.y;
}
if (startPoint.y > modifiedBounds.b) {
modifiedBounds.b = startPoint.y;
}
for (ptrdiff_t delta = -1; delta <= 1; delta += 2) {
EsPoint point = startPoint;
uint32_t *pointer = bits + point.y * stride + point.x;
bool spaceAbove = false;
bool spaceBelow = false;
if (delta == 1) {
point.x += delta;
pointer += delta;
if (point.x == (int32_t) width) {
break;
}
}
while (true) {
if (*pointer != replaceColor) {
break;
}
*pointer = color;
if (point.x < modifiedBounds.l) {
modifiedBounds.l = point.x;
}
if (point.x > modifiedBounds.r) {
modifiedBounds.r = point.x;
}
if (point.y) {
if (!spaceAbove && pointer[-stride] == replaceColor) {
spaceAbove = true;
pointsToVisit.Add({ point.x, point.y - 1 });
} else if (spaceAbove && pointer[-stride] != replaceColor) {
spaceAbove = false;
}
}
if (point.y != (int32_t) height - 1) {
if (!spaceBelow && pointer[stride] == replaceColor) {
spaceBelow = true;
pointsToVisit.Add({ point.x, point.y + 1 });
} else if (spaceBelow && pointer[stride] != replaceColor) {
spaceBelow = false;
}
}
point.x += delta;
pointer += delta;
if (point.x == (int32_t) width || point.x < 0) {
break;
}
}
}
}
modifiedBounds.r++, modifiedBounds.b += 2;
pointsToVisit.Free();
EsPaintTargetEndDirectAccess(instance->bitmap);
EsElementRepaint(instance->canvas, &modifiedBounds);
return modifiedBounds;
}
EsRectangle DrawLine(Instance *instance, bool force = false) {
EsPoint point = EsMouseGetPosition(instance->canvas);
float brushSize = EsTextboxGetContentsAsDouble(instance->brushSize);
float spacing = brushSize * 0.1f;
uint32_t color = 0xFF000000 | EsColorWellGetRGB(instance->colorWell);
EsRectangle modifiedBounds = ES_RECT_4(instance->bitmapWidth, 0, instance->bitmapHeight, 0);
uint32_t *bits;
size_t width, height, stride;
EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
stride /= 4;
// Draw the line.
while (true) {
float dx = point.x - instance->previousPointX;
float dy = point.y - instance->previousPointY;
float distance = EsCRTsqrtf(dx * dx + dy * dy);
if (distance < spacing && !force) {
break;
}
int32_t x0 = instance->previousPointX;
int32_t y0 = instance->previousPointY;
EsRectangle bounds = ES_RECT_4(x0 - brushSize / 2 - 1, x0 + brushSize / 2 + 1,
y0 - brushSize / 2 - 1, y0 + brushSize / 2 + 1);
bounds = EsRectangleIntersection(bounds, ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight));
modifiedBounds = EsRectangleBounding(modifiedBounds, bounds);
for (int32_t y = bounds.t; y < bounds.b; y++) {
for (int32_t x = bounds.l; x < bounds.r; x++) {
float distance = (x - x0) * (x - x0) + (y - y0) * (y - y0);
if (distance < brushSize * brushSize * 0.25f) {
bits[y * stride + x] = color;
}
}
}
if (force) {
break;
}
instance->previousPointX += dx / distance * spacing;
instance->previousPointY += dy / distance * spacing;
}
// Repaint the canvas.
modifiedBounds.r++, modifiedBounds.b++;
EsPaintTargetEndDirectAccess(instance->bitmap);
EsElementRepaint(instance->canvas, &modifiedBounds);
return modifiedBounds;
}
int CanvasMessage(EsElement *element, EsMessage *message) {
Instance *instance = element->instance;
if (message->type == ES_MSG_PAINT) {
EsPainter *painter = message->painter;
EsRectangle bounds = EsPainterBoundsInset(painter);
EsRectangle area = ES_RECT_4(bounds.l, bounds.l + instance->bitmapWidth, bounds.t, bounds.t + instance->bitmapHeight);
EsDrawPaintTarget(painter, instance->bitmap, area, ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight), 0xFF);
if (instance->commandRectangle.check == ES_CHECK_CHECKED && instance->dragged) {
EsRectangle rectangle = instance->modifiedBounds;
rectangle.l += painter->offsetX, rectangle.r += painter->offsetX;
rectangle.t += painter->offsetY, rectangle.b += painter->offsetY;
EsDrawBlock(painter, EsRectangleIntersection(rectangle, area), 0xFF000000 | EsColorWellGetRGB(instance->colorWell));
}
} else if ((message->type == ES_MSG_MOUSE_LEFT_DRAG || message->type == ES_MSG_MOUSE_MOVED)
&& EsMouseIsLeftHeld() && instance->commandBrush.check == ES_CHECK_CHECKED) {
instance->modifiedBounds = EsRectangleBounding(DrawLine(instance), instance->modifiedBounds);
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG && instance->commandRectangle.check == ES_CHECK_CHECKED) {
EsRectangle previous = instance->modifiedBounds;
EsPoint point = EsMouseGetPosition(element);
EsRectangle rectangle;
if (point.x < instance->previousPointX) rectangle.l = point.x, rectangle.r = instance->previousPointX + 1;
else rectangle.l = instance->previousPointX, rectangle.r = point.x + 1;
if (point.y < instance->previousPointY) rectangle.t = point.y, rectangle.b = instance->previousPointY + 1;
else rectangle.t = instance->previousPointY, rectangle.b = point.y + 1;
instance->modifiedBounds = rectangle;
EsRectangle bounding = EsRectangleBounding(rectangle, previous);
EsElementRepaint(element, &bounding);
instance->dragged = true;
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN && instance->commandBrush.check == ES_CHECK_CHECKED) {
EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
instance->image = ImageFork(instance, instance->image);
EsPoint point = EsMouseGetPosition(element);
instance->previousPointX = point.x, instance->previousPointY = point.y;
instance->modifiedBounds = ES_RECT_4(instance->bitmapWidth, 0, instance->bitmapHeight, 0);
DrawLine(instance, true);
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN && instance->commandRectangle.check == ES_CHECK_CHECKED) {
EsPoint point = EsMouseGetPosition(element);
instance->previousPointX = point.x, instance->previousPointY = point.y;
instance->dragged = false;
} else if (message->type == ES_MSG_MOUSE_LEFT_UP && instance->commandBrush.check == ES_CHECK_CHECKED) {
ImageCopyFromPaintTarget(instance, &instance->image, instance->modifiedBounds);
} else if (message->type == ES_MSG_MOUSE_LEFT_UP && instance->commandRectangle.check == ES_CHECK_CHECKED && instance->dragged) {
instance->dragged = false;
EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
instance->image = ImageFork(instance, instance->image);
EsPainter painter = {};
painter.clip = ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight);
painter.target = instance->bitmap;
EsDrawBlock(&painter, instance->modifiedBounds, 0xFF000000 | EsColorWellGetRGB(instance->colorWell));
ImageCopyFromPaintTarget(instance, &instance->image, instance->modifiedBounds);
} else if (message->type == ES_MSG_MOUSE_LEFT_UP && instance->commandFill.check == ES_CHECK_CHECKED) {
EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
instance->image = ImageFork(instance, instance->image);
EsRectangle modifiedBounds = DrawFill(instance, EsMouseGetPosition(element));
ImageCopyFromPaintTarget(instance, &instance->image, modifiedBounds);
} else if (message->type == ES_MSG_GET_CURSOR) {
message->cursorStyle = ES_CURSOR_CROSS_HAIR_PICK;
} else if (message->type == ES_MSG_GET_WIDTH) {
message->measure.width = instance->bitmapWidth;
} else if (message->type == ES_MSG_GET_HEIGHT) {
message->measure.height = instance->bitmapHeight;
} else {
return 0;
}
return ES_HANDLED;
}
void CommandSelectTool(Instance *instance, EsElement *, EsCommand *command) {
if (command->check == ES_CHECK_CHECKED) {
return;
}
EsCommandSetCheck(&instance->commandBrush, ES_CHECK_UNCHECKED, false);
EsCommandSetCheck(&instance->commandFill, ES_CHECK_UNCHECKED, false);
EsCommandSetCheck(&instance->commandRectangle, ES_CHECK_UNCHECKED, false);
EsCommandSetCheck(&instance->commandSelect, ES_CHECK_UNCHECKED, false);
EsCommandSetCheck(&instance->commandText, ES_CHECK_UNCHECKED, false);
EsCommandSetCheck(command, ES_CHECK_CHECKED, false);
if (command == &instance->commandBrush) EsButtonSetIcon(instance->toolDropdown, ES_ICON_DRAW_FREEHAND);
if (command == &instance->commandFill) EsButtonSetIcon(instance->toolDropdown, ES_ICON_COLOR_FILL);
if (command == &instance->commandRectangle) EsButtonSetIcon(instance->toolDropdown, ES_ICON_DRAW_RECTANGLE);
if (command == &instance->commandSelect) EsButtonSetIcon(instance->toolDropdown, ES_ICON_OBJECT_GROUP);
if (command == &instance->commandText) EsButtonSetIcon(instance->toolDropdown, ES_ICON_DRAW_TEXT);
instance->dragged = false;
}
int BitmapSizeTextboxMessage(EsElement *element, EsMessage *message) {
EsTextbox *textbox = (EsTextbox *) element;
Instance *instance = textbox->instance;
if (message->type == ES_MSG_TEXTBOX_EDIT_END || message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_END) {
char *expression = EsTextboxGetContents(textbox);
EsCalculationValue value = EsCalculateFromUserExpression(expression);
EsHeapFree(expression);
if (value.error) {
return ES_REJECTED;
}
if (value.number < 1) value.number = 1;
else if (value.number > 20000) value.number = 20000;
int newSize = (int) (value.number + 0.5);
char result[64];
size_t resultBytes = EsStringFormat(result, sizeof(result), "%d", newSize);
EsTextboxSelectAll(textbox);
EsTextboxInsert(textbox, result, resultBytes);
int oldSize = textbox->userData.i ? instance->bitmapHeight : instance->bitmapWidth;
if (oldSize == newSize) {
return ES_HANDLED;
}
EsRectangle clearRegion;
if (textbox->userData.i) {
instance->bitmapHeight = newSize;
clearRegion = ES_RECT_4(0, instance->bitmapWidth, oldSize, newSize);
} else {
instance->bitmapWidth = newSize;
clearRegion = ES_RECT_4(oldSize, newSize, 0, instance->bitmapHeight);
}
EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
instance->image = ImageFork(instance, instance->image, instance->bitmapWidth, instance->bitmapHeight);
EsPaintTargetDestroy(instance->bitmap);
instance->bitmap = EsPaintTargetCreate(instance->bitmapWidth, instance->bitmapHeight, false);
ImageCopyToPaintTarget(instance, &instance->image);
EsPainter painter = {};
painter.clip = ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight);
painter.target = instance->bitmap;
EsDrawBlock(&painter, clearRegion, 0xFFFFFFFF);
ImageCopyFromPaintTarget(instance, &instance->image, clearRegion);
EsElementRelayout(EsElementGetLayoutParent(instance->canvas));
return ES_HANDLED;
} else if (message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA) {
int oldValue = EsTextboxGetContentsAsDouble(textbox);
int newValue = oldValue + message->numberDragDelta.delta * (message->numberDragDelta.fast ? 10 : 1);
if (newValue < 1) newValue = 1;
else if (newValue > 20000) newValue = 20000;
char result[64];
size_t resultBytes = EsStringFormat(result, sizeof(result), "%d", newValue);
EsTextboxSelectAll(textbox);
EsTextboxInsert(textbox, result, resultBytes);
return ES_HANDLED;
}
return 0;
}
void ImageTransform(EsMenu *menu, EsGeneric context) {
Instance *instance = menu->instance;
EsUndoPush(instance->undoManager, ImageUndoMessage, &instance->image, sizeof(Image));
uint32_t *bits;
size_t width, height, stride;
EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
EsPaintTarget *newTarget = nullptr;
uint32_t *newBits = nullptr;
size_t newStride = 0;
if (context.i == 1 || context.i == 2) {
instance->image = ImageFork(instance, instance->image, height, width);
newTarget = EsPaintTargetCreate(height, width, false);
EsPaintTargetStartDirectAccess(newTarget, &newBits, &height, &width, &newStride);
} else {
instance->image = ImageFork(instance, instance->image);
}
if (context.i == 1 /* rotate left */) {
for (uintptr_t i = 0; i < height; i++) {
for (uintptr_t j = 0; j < width; j++) {
newBits[(width - j - 1) * newStride / 4 + i] = bits[i * stride / 4 + j];
}
}
} else if (context.i == 2 /* rotate right */) {
for (uintptr_t i = 0; i < height; i++) {
for (uintptr_t j = 0; j < width; j++) {
newBits[j * newStride / 4 + (height - i - 1)] = bits[i * stride / 4 + j];
}
}
} else if (context.i == 3 /* flip horizontally */) {
for (uintptr_t i = 0; i < height; i++) {
for (uintptr_t j = 0; j < width / 2; j++) {
uint32_t temporary = bits[i * stride / 4 + j];
bits[i * stride / 4 + j] = bits[i * stride / 4 + (width - j - 1)];
bits[i * stride / 4 + (width - j - 1)] = temporary;
}
}
} else if (context.i == 4 /* flip vertically */) {
for (uintptr_t i = 0; i < height / 2; i++) {
for (uintptr_t j = 0; j < width; j++) {
uint32_t temporary = bits[i * stride / 4 + j];
bits[i * stride / 4 + j] = bits[(height - i - 1) * stride / 4 + j];
bits[(height - i - 1) * stride / 4 + j] = temporary;
}
}
}
EsPaintTargetEndDirectAccess(instance->bitmap);
if (newTarget) {
EsPaintTargetDestroy(instance->bitmap);
instance->bitmap = newTarget;
size_t width, height;
EsPaintTargetGetSize(instance->bitmap, &width, &height);
instance->bitmapWidth = width;
instance->bitmapHeight = height;
EsElementRelayout(EsElementGetLayoutParent(instance->canvas));
}
ImageCopyFromPaintTarget(instance, &instance->image, ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight));
EsElementRepaint(instance->canvas);
}
void MenuTools(Instance *instance, EsElement *element, EsCommand *) {
EsMenu *menu = EsMenuCreate(element);
EsMenuAddCommandsFromToolbar(menu, instance->toolPanel);
EsMenuShow(menu);
}
void MenuImage(Instance *instance, EsElement *element, EsCommand *) {
EsMenu *menu = EsMenuCreate(element);
EsMenuAddItem(menu, ES_MENU_ITEM_HEADER, INTERFACE_STRING(ImageEditorCanvasSize));
EsPanel *table = EsPanelCreate(menu, ES_PANEL_HORIZONTAL | ES_PANEL_TABLE, EsStyleIntern(&styleImageMenuTable));
EsPanelSetBands(table, 2, 2);
char buffer[64];
size_t bytes;
EsTextbox *textbox;
bytes = EsStringFormat(buffer, sizeof(buffer), "%d", instance->bitmapWidth);
EsTextDisplayCreate(table, ES_CELL_H_RIGHT, ES_STYLE_TEXT_LABEL, INTERFACE_STRING(ImageEditorPropertyWidth));
textbox = EsTextboxCreate(table, ES_TEXTBOX_EDIT_BASED, ES_STYLE_TEXTBOX_BORDERED_SINGLE_MEDIUM);
EsTextboxInsert(textbox, buffer, bytes, false);
textbox->userData.i = 0;
textbox->messageUser = BitmapSizeTextboxMessage;
EsTextboxUseNumberOverlay(textbox, true);
bytes = EsStringFormat(buffer, sizeof(buffer), "%d", instance->bitmapHeight);
EsTextDisplayCreate(table, ES_CELL_H_RIGHT, ES_STYLE_TEXT_LABEL, INTERFACE_STRING(ImageEditorPropertyHeight));
textbox = EsTextboxCreate(table, ES_TEXTBOX_EDIT_BASED, ES_STYLE_TEXTBOX_BORDERED_SINGLE_MEDIUM);
EsTextboxInsert(textbox, buffer, bytes, false);
textbox->userData.i = 1;
textbox->messageUser = BitmapSizeTextboxMessage;
EsTextboxUseNumberOverlay(textbox, true);
EsMenuAddSeparator(menu);
EsMenuAddItem(menu, ES_MENU_ITEM_HEADER, INTERFACE_STRING(ImageEditorImageTransformations));
EsMenuAddItem(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(ImageEditorRotateLeft), ImageTransform, 1);
EsMenuAddItem(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(ImageEditorRotateRight), ImageTransform, 2);
EsMenuAddItem(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(ImageEditorFlipHorizontally), ImageTransform, 3);
EsMenuAddItem(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(ImageEditorFlipVertically), ImageTransform, 4);
EsMenuShow(menu);
}
void WriteCallback(void *context, void *data, int size) {
EsBufferWrite((EsBuffer *) context, data, size);
}
void SwapRedAndBlueChannels(uint32_t *bits, size_t width, size_t height, size_t stride) {
for (uintptr_t i = 0; i < height; i++) {
for (uintptr_t j = 0; j < width; j++) {
uint32_t *pixel = &bits[i * stride / 4 + j];
*pixel = (*pixel & 0xFF00FF00) | (((*pixel >> 16) | (*pixel << 16)) & 0x00FF00FF);
}
}
}
int InstanceCallback(Instance *instance, EsMessage *message) {
if (message->type == ES_MSG_INSTANCE_DESTROY) {
EsPaintTargetDestroy(instance->bitmap);
ImageDelete(instance->image);
} else if (message->type == ES_MSG_INSTANCE_SAVE) {
// TODO Error handling.
uintptr_t extensionOffset = message->instanceSave.nameOrPathBytes;
while (extensionOffset) {
if (message->instanceSave.nameOrPath[extensionOffset - 1] == '.') {
break;
} else {
extensionOffset--;
}
}
const char *extension = extensionOffset ? message->instanceSave.nameOrPath + extensionOffset : "png";
size_t extensionBytes = extensionOffset ? message->instanceSave.nameOrPathBytes - extensionOffset : 3;
uint32_t *bits;
size_t width, height, stride;
EsPaintTargetStartDirectAccess(instance->bitmap, &bits, &width, &height, &stride);
EsAssert(stride == width * 4); // TODO Other strides.
SwapRedAndBlueChannels(bits, width, height, stride); // stbi_write uses the other order. We swap back below.
size_t _bufferBytes = 262144;
uint8_t *_buffer = (uint8_t *) EsHeapAllocate(_bufferBytes, false);
EsBuffer buffer = { .out = _buffer, .bytes = _bufferBytes };
buffer.fileStore = message->instanceSave.file;
EsUniqueIdentifier typeJPG = (EsUniqueIdentifier)
{{ 0xD8, 0xC2, 0x13, 0xB0, 0x53, 0x64, 0x82, 0x11, 0x48, 0x7B, 0x5B, 0x64, 0x0F, 0x92, 0xB9, 0x38 }};
EsUniqueIdentifier typeBMP = (EsUniqueIdentifier)
{{ 0x40, 0x15, 0xB7, 0x82, 0x99, 0x6D, 0xD5, 0x41, 0x96, 0xD5, 0x3B, 0x6D, 0xA6, 0x5F, 0x07, 0x34 }};
EsUniqueIdentifier typeTGA = (EsUniqueIdentifier)
{{ 0x69, 0x62, 0x4E, 0x28, 0xA1, 0xE1, 0x7B, 0x35, 0x64, 0x2E, 0x36, 0x01, 0x65, 0x91, 0xBE, 0xA1 }};
EsUniqueIdentifier typePNG = (EsUniqueIdentifier)
{{ 0x59, 0x21, 0x05, 0x4D, 0x34, 0x40, 0xAB, 0x61, 0xEC, 0xF9, 0x7D, 0x5C, 0x6E, 0x04, 0x96, 0xAE }};
if (0 == EsStringCompare(extension, extensionBytes, EsLiteral("jpg"))
|| 0 == EsStringCompare(extension, extensionBytes, EsLiteral("jpeg"))) {
EsFileStoreSetContentType(message->instanceSave.file, typeJPG);
stbi_write_jpg_to_func(WriteCallback, &buffer, width, height, 4, bits, 90);
} else if (0 == EsStringCompare(extension, extensionBytes, EsLiteral("bmp"))) {
EsFileStoreSetContentType(message->instanceSave.file, typeBMP);
stbi_write_bmp_to_func(WriteCallback, &buffer, width, height, 4, bits);
} else if (0 == EsStringCompare(extension, extensionBytes, EsLiteral("tga"))) {
EsFileStoreSetContentType(message->instanceSave.file, typeTGA);
stbi_write_tga_to_func(WriteCallback, &buffer, width, height, 4, bits);
} else {
EsFileStoreSetContentType(message->instanceSave.file, typePNG);
stbi_write_png_to_func(WriteCallback, &buffer, width, height, 4, bits, stride);
}
SwapRedAndBlueChannels(bits, width, height, stride); // Swap back.
EsBufferFlushToFileStore(&buffer);
EsHeapFree(_buffer);
EsPaintTargetEndDirectAccess(instance->bitmap);
EsInstanceSaveComplete(instance, message->instanceSave.file, true);
} else if (message->type == ES_MSG_INSTANCE_OPEN) {
size_t fileSize;
uint8_t *file = (uint8_t *) EsFileStoreReadAll(message->instanceOpen.file, &fileSize);
if (!file) {
EsInstanceOpenComplete(instance, message->instanceOpen.file, false);
return ES_HANDLED;
}
uint32_t width, height;
uint8_t *bits = EsImageLoad(file, fileSize, &width, &height, 4);
EsHeapFree(file);
if (!bits) {
EsInstanceOpenComplete(instance, message->instanceOpen.file, false, INTERFACE_STRING(ImageEditorUnsupportedFormat));
return ES_HANDLED;
}
EsPaintTargetDestroy(instance->bitmap);
ImageDelete(instance->image);
instance->bitmapWidth = width;
instance->bitmapHeight = height;
instance->bitmap = EsPaintTargetCreate(instance->bitmapWidth, instance->bitmapHeight, false);
EsPainter painter = {};
painter.clip = ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight);
painter.target = instance->bitmap;
EsDrawBitmap(&painter, painter.clip, (uint32_t *) bits, width * 4, 0xFF);
instance->image = ImageFork(instance, {}, instance->bitmapWidth, instance->bitmapHeight);
ImageCopyFromPaintTarget(instance, &instance->image, painter.clip);
EsElementRelayout(EsElementGetLayoutParent(instance->canvas));
EsHeapFree(bits);
EsInstanceOpenComplete(instance, message->instanceOpen.file, true);
} else {
return 0;
}
return ES_HANDLED;
}
void InstanceCreate(EsMessage *message) {
Instance *instance = EsInstanceCreate(message, INTERFACE_STRING(ImageEditorTitle));
instance->callback = InstanceCallback;
EsElement *toolbar = EsWindowGetToolbar(instance->window);
EsInstanceSetClassEditor(instance, &editorSettings);
// Register commands.
EsCommandRegister(&instance->commandBrush, instance, INTERFACE_STRING(ImageEditorToolBrush), CommandSelectTool, 1, "N", true);
EsCommandRegister(&instance->commandFill, instance, INTERFACE_STRING(ImageEditorToolFill), CommandSelectTool, 2, "Shift+B", true);
EsCommandRegister(&instance->commandRectangle, instance, INTERFACE_STRING(ImageEditorToolRectangle), CommandSelectTool, 3, "Shift+R", true);
EsCommandRegister(&instance->commandSelect, instance, INTERFACE_STRING(ImageEditorToolSelect), CommandSelectTool, 4, "R", false);
EsCommandRegister(&instance->commandText, instance, INTERFACE_STRING(ImageEditorToolText), CommandSelectTool, 5, "T", false);
EsCommandSetCheck(&instance->commandBrush, ES_CHECK_CHECKED, false);
// Create the toolbar.
EsButton *button;
EsFileMenuAddToToolbar(toolbar);
button = EsButtonCreate(toolbar, ES_BUTTON_DROPDOWN, ES_STYLE_PUSH_BUTTON_TOOLBAR, INTERFACE_STRING(ImageEditorImage));
EsButtonSetIcon(button, ES_ICON_IMAGE_X_GENERIC);
button->accessKey = 'I';
EsButtonOnCommand(button, MenuImage);
EsPanel *buttonGroup = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL | ES_ELEMENT_AUTO_GROUP);
button = EsButtonCreate(buttonGroup);
EsCommandAddButton(EsCommandByID(instance, ES_COMMAND_UNDO), button);
EsButtonSetIcon(button, ES_ICON_EDIT_UNDO_SYMBOLIC);
button->accessKey = 'U';
EsSpacerCreate(buttonGroup, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
button = EsButtonCreate(buttonGroup);
EsCommandAddButton(EsCommandByID(instance, ES_COMMAND_REDO), button);
EsButtonSetIcon(button, ES_ICON_EDIT_REDO_SYMBOLIC);
button->accessKey = 'R';
EsSpacerCreate(toolbar, ES_CELL_FILL);
button = instance->toolDropdown = EsButtonCreate(toolbar, ES_BUTTON_DROPDOWN,
ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorPickTool));
EsButtonSetIcon(button, ES_ICON_DRAW_FREEHAND);
EsButtonOnCommand(button, MenuTools);
button->accessKey = 'T';
instance->toolPanel = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL | ES_ELEMENT_AUTO_GROUP);
button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolBrush));
EsCommandAddButton(&instance->commandBrush, button);
EsButtonSetIcon(button, ES_ICON_DRAW_FREEHAND);
button->accessKey = 'B';
EsSpacerCreate(instance->toolPanel, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolFill));
EsCommandAddButton(&instance->commandFill, button);
EsButtonSetIcon(button, ES_ICON_COLOR_FILL);
button->accessKey = 'F';
EsSpacerCreate(instance->toolPanel, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolRectangle));
EsCommandAddButton(&instance->commandRectangle, button);
EsButtonSetIcon(button, ES_ICON_DRAW_RECTANGLE);
button->accessKey = 'E';
EsSpacerCreate(instance->toolPanel, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolSelect));
EsCommandAddButton(&instance->commandSelect, button);
EsButtonSetIcon(button, ES_ICON_OBJECT_GROUP);
button->accessKey = 'S';
EsSpacerCreate(instance->toolPanel, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR);
button = EsButtonCreate(instance->toolPanel, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG, INTERFACE_STRING(ImageEditorToolText));
EsCommandAddButton(&instance->commandText, button);
EsButtonSetIcon(button, ES_ICON_DRAW_TEXT);
button->accessKey = 'T';
EsWindowAddSizeAlternative(instance->window, instance->toolDropdown, instance->toolPanel, 1100, 0);
EsSpacerCreate(toolbar, ES_CELL_FILL);
EsPanel *section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, INTERFACE_STRING(ImageEditorPropertyColor));
instance->colorWell = EsColorWellCreate(section, ES_FLAGS_DEFAULT, 0xFFFF0000);
instance->colorWell->accessKey = 'C';
EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, 0, 5, 0);
section = EsPanelCreate(toolbar, ES_PANEL_HORIZONTAL);
EsTextDisplayCreate(section, ES_FLAGS_DEFAULT, 0, INTERFACE_STRING(ImageEditorPropertyBrushSize));
instance->brushSize = EsTextboxCreate(section, ES_TEXTBOX_EDIT_BASED, ES_STYLE_TEXTBOX_BORDERED_SINGLE_COMPACT);
instance->brushSize->messageUser = BrushSizeMessage;
EsTextboxUseNumberOverlay(instance->brushSize, false);
EsTextboxInsert(instance->brushSize, EsLiteral("5.0"));
instance->brushSize->accessKey = 'Z';
EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, 0, 1, 0);
// Create the user interface.
EsWindowSetIcon(instance->window, ES_ICON_MULTIMEDIA_PHOTO_MANAGER);
EsCanvasPane *canvasPane = EsCanvasPaneCreate(instance->window, ES_CELL_FILL | ES_CANVAS_PANE_SHOW_SHADOW, ES_STYLE_PANEL_WINDOW_BACKGROUND);
instance->canvas = EsCustomElementCreate(canvasPane, ES_CELL_CENTER | ES_ELEMENT_FOCUSABLE);
instance->canvas->messageUser = CanvasMessage;
EsElementFocus(instance->canvas, false);
// Setup the paint target and the image.
instance->bitmapWidth = 500;
instance->bitmapHeight = 400;
instance->bitmap = EsPaintTargetCreate(instance->bitmapWidth, instance->bitmapHeight, false);
EsPainter painter = {};
painter.clip = ES_RECT_4(0, instance->bitmapWidth, 0, instance->bitmapHeight);
painter.target = instance->bitmap;
EsDrawBlock(&painter, painter.clip, 0xFFFFFFFF);
instance->image = ImageFork(instance, {}, instance->bitmapWidth, instance->bitmapHeight);
ImageCopyFromPaintTarget(instance, &instance->image, painter.clip);
}
void _start() {
_init();
while (true) {
EsMessage *message = EsMessageReceive();
if (message->type == ES_MSG_INSTANCE_CREATE) {
InstanceCreate(message);
}
}
}