mirror of
https://github.com/jtheoof/swappy.git
synced 2024-07-14 18:00:25 +03:00
fix(blur): use rendered surface after commit
- Fix blur logic - Reused rendered surface to optimize future render on comitted blur - Include gaussian kernel function Closes #20 Closes #22
This commit is contained in:
parent
416b0adad9
commit
46fb08dce1
13
include/algebra.h
Normal file
13
include/algebra.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
struct gaussian_kernel {
|
||||
gdouble *kernel;
|
||||
gint size;
|
||||
gdouble sigma;
|
||||
gdouble sum;
|
||||
};
|
||||
|
||||
struct gaussian_kernel *gaussian_kernel(gint width, gdouble sigma);
|
||||
void gaussian_kernel_free(gpointer data);
|
@ -79,9 +79,10 @@ struct swappy_paint_brush {
|
||||
};
|
||||
|
||||
struct swappy_paint_blur {
|
||||
double bluriness;
|
||||
double blur_level;
|
||||
struct swappy_point from;
|
||||
struct swappy_point to;
|
||||
cairo_surface_t *surface;
|
||||
};
|
||||
|
||||
struct swappy_paint {
|
||||
|
@ -5,3 +5,4 @@
|
||||
|
||||
void string_remove_at(char *str, size_t pos);
|
||||
gchar *string_insert_chars_at(gchar *str, gchar *chars, size_t pos);
|
||||
void pixel_data_print(guint32 pixel);
|
||||
|
@ -51,6 +51,7 @@ executable(
|
||||
swappy_resources,
|
||||
files([
|
||||
'src/main.c',
|
||||
'src/algebra.c',
|
||||
'src/application.c',
|
||||
'src/buffer.c',
|
||||
'src/box.c',
|
||||
|
37
src/algebra.c
Normal file
37
src/algebra.c
Normal file
@ -0,0 +1,37 @@
|
||||
#include "algebra.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <math.h>
|
||||
|
||||
struct gaussian_kernel *gaussian_kernel(int width, double sigma) {
|
||||
double sum = 0;
|
||||
gint size = width * width + 1;
|
||||
double *kernel = g_new(double, size);
|
||||
struct gaussian_kernel *gaussian = g_new(struct gaussian_kernel, 1);
|
||||
for (gint y = 0; y < width; y++) {
|
||||
for (gint x = 0; x < width; x++) {
|
||||
double j = y - width;
|
||||
double i = x - width;
|
||||
double cell = ((1.0 / (2.0 * G_PI * sigma)) *
|
||||
exp((-(i * i + j * j)) / (2.0 * sigma * sigma))) *
|
||||
0xff;
|
||||
kernel[y * width + x] = cell;
|
||||
sum += cell;
|
||||
}
|
||||
}
|
||||
|
||||
gaussian->kernel = kernel;
|
||||
gaussian->size = size;
|
||||
gaussian->sigma = sigma;
|
||||
gaussian->sum = sum;
|
||||
|
||||
return gaussian;
|
||||
}
|
||||
|
||||
void gaussian_kernel_free(gpointer data) {
|
||||
struct gaussian_kernel *gaussian = (struct gaussian_kernel *)data;
|
||||
if (gaussian != NULL) {
|
||||
g_free(gaussian->kernel);
|
||||
g_free(gaussian);
|
||||
}
|
||||
}
|
@ -406,11 +406,18 @@ gboolean draw_area_configure_handler(GtkWidget *widget,
|
||||
g_debug("received configure_event handler");
|
||||
cairo_surface_destroy(state->cairo_surface);
|
||||
|
||||
state->cairo_surface = gdk_window_create_similar_surface(
|
||||
gtk_widget_get_window(widget), CAIRO_CONTENT_COLOR,
|
||||
cairo_surface_t *surface = gdk_window_create_similar_surface(
|
||||
gtk_widget_get_window(widget), CAIRO_CONTENT_COLOR_ALPHA,
|
||||
gtk_widget_get_allocated_width(widget),
|
||||
gtk_widget_get_allocated_height(widget));
|
||||
|
||||
g_info("size of cairo_surface: %ux%u with type: %d",
|
||||
cairo_image_surface_get_width(surface),
|
||||
cairo_image_surface_get_height(surface),
|
||||
cairo_image_surface_get_format(surface));
|
||||
|
||||
state->cairo_surface = surface;
|
||||
|
||||
render_state(state);
|
||||
|
||||
return TRUE;
|
||||
|
16
src/paint.c
16
src/paint.c
@ -23,6 +23,9 @@ void paint_free(gpointer data) {
|
||||
|
||||
switch (paint->type) {
|
||||
case SWAPPY_PAINT_MODE_BLUR:
|
||||
if (paint->content.blur.surface) {
|
||||
cairo_surface_destroy(paint->content.blur.surface);
|
||||
}
|
||||
break;
|
||||
case SWAPPY_PAINT_MODE_BRUSH:
|
||||
g_list_free_full(paint->content.brush.points, g_free);
|
||||
@ -70,7 +73,7 @@ void paint_add_temporary(struct swappy_state *state, double x, double y,
|
||||
if (type == SWAPPY_PAINT_MODE_TEXT) {
|
||||
paint_commit_temporary(state);
|
||||
} else {
|
||||
g_free(state->temp_paint);
|
||||
paint_free(state->temp_paint);
|
||||
state->temp_paint = NULL;
|
||||
}
|
||||
}
|
||||
@ -79,9 +82,10 @@ void paint_add_temporary(struct swappy_state *state, double x, double y,
|
||||
case SWAPPY_PAINT_MODE_BLUR:
|
||||
paint->can_draw = false;
|
||||
|
||||
paint->content.blur.bluriness = state->settings.blur_level;
|
||||
paint->content.blur.blur_level = state->settings.blur_level;
|
||||
paint->content.blur.from.x = x;
|
||||
paint->content.blur.from.y = y;
|
||||
paint->content.blur.surface = NULL;
|
||||
break;
|
||||
case SWAPPY_PAINT_MODE_BRUSH:
|
||||
paint->can_draw = true;
|
||||
@ -147,6 +151,14 @@ void paint_update_temporary_shape(struct swappy_state *state, double x,
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t width = state->window->width;
|
||||
int32_t height = state->window->height;
|
||||
|
||||
// Bounding x and y to the window dimensions to avoid side effects in
|
||||
// rendering.
|
||||
x = fmin(fmax(x, 0), width);
|
||||
y = fmin(fmax(y, 0), height);
|
||||
|
||||
switch (paint->type) {
|
||||
case SWAPPY_PAINT_MODE_BLUR:
|
||||
paint->can_draw = true;
|
||||
|
249
src/render.c
249
src/render.c
@ -1,157 +1,152 @@
|
||||
#include <gdk/gdk.h>
|
||||
#include <glib.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <math.h>
|
||||
#include <pango/pangocairo.h>
|
||||
|
||||
#include "algebra.h"
|
||||
#include "swappy.h"
|
||||
#include "util.h"
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI (3.14159265358979323846)
|
||||
#endif
|
||||
|
||||
#define RENDER_PANGO_FONT SWAPPY_TEXT_FONT_DEFAULT SWAPPY_TEXT_SIZE_DEFAULT
|
||||
|
||||
#define pango_layout_t PangoLayout
|
||||
#define pango_font_description_t PangoFontDescription
|
||||
#define pango_rectangle_t PangoRectangle
|
||||
|
||||
#define ARRAY_LENGTH(a) (sizeof(a) / sizeof(a)[0])
|
||||
|
||||
static struct swappy_point swappy_point_scaled(struct swappy_point point,
|
||||
gint scale) {
|
||||
struct swappy_point ret = {
|
||||
.x = point.x * scale,
|
||||
.y = point.y * scale,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* This code was largely taken from Kristian Høgsberg and Chris Wilson from:
|
||||
* https://www.cairographics.org/cookbook/blur.c/
|
||||
*/
|
||||
static void blur_paint(cairo_t *cr, struct swappy_paint_blur *blur,
|
||||
gint scaling_factor) {
|
||||
cairo_surface_t *tmp;
|
||||
int width, height;
|
||||
static cairo_surface_t *blur_surface(cairo_surface_t *surface, double x,
|
||||
double y, double width, double height,
|
||||
gint blur_level) {
|
||||
cairo_surface_t *dest_surface, *tmp_surface;
|
||||
cairo_t *cr;
|
||||
int src_width, src_height;
|
||||
int src_stride, dst_stride;
|
||||
int x, y, z, w;
|
||||
uint8_t *src, *dst;
|
||||
uint32_t *s, *d, a, p;
|
||||
guint u, v, w, z;
|
||||
uint8_t *src, *dst, *tmp;
|
||||
uint32_t *s, *d, p;
|
||||
int i, j, k;
|
||||
uint8_t kernel[17];
|
||||
const int size = ARRAY_LENGTH(kernel);
|
||||
const int size = (int)blur_level * 2 + 1;
|
||||
const int half = size / 2;
|
||||
double bluriness = blur->bluriness;
|
||||
struct swappy_point from = swappy_point_scaled(blur->from, scaling_factor);
|
||||
struct swappy_point to = swappy_point_scaled(blur->to, scaling_factor);
|
||||
const double offset_y = 10.0;
|
||||
guint sum;
|
||||
|
||||
cairo_surface_t *surface = cairo_get_target(cr);
|
||||
if (cairo_surface_status(surface)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (cairo_surface_status(surface)) return;
|
||||
|
||||
width = cairo_image_surface_get_width(surface);
|
||||
height = cairo_image_surface_get_height(surface);
|
||||
|
||||
switch (cairo_image_surface_get_format(surface)) {
|
||||
cairo_format_t src_format = cairo_image_surface_get_format(surface);
|
||||
switch (src_format) {
|
||||
case CAIRO_FORMAT_A1:
|
||||
default:
|
||||
/* Don't even think about it! */
|
||||
return;
|
||||
|
||||
case CAIRO_FORMAT_A8:
|
||||
/* Handle a8 surfaces by effectively unrolling the loops by a
|
||||
* factor of 4 - this is safe since we know that stride has to be a
|
||||
* multiple of uint32_t. */
|
||||
width /= 4;
|
||||
break;
|
||||
|
||||
case CAIRO_FORMAT_RGB24:
|
||||
default:
|
||||
g_warning("source surface format: %d is not supported", src_format);
|
||||
return NULL;
|
||||
case CAIRO_FORMAT_ARGB32:
|
||||
break;
|
||||
}
|
||||
|
||||
tmp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
|
||||
if (cairo_surface_status(tmp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
src = cairo_image_surface_get_data(surface);
|
||||
src_stride = cairo_image_surface_get_stride(surface);
|
||||
src_width = cairo_image_surface_get_width(surface);
|
||||
src_height = cairo_image_surface_get_height(surface);
|
||||
|
||||
g_debug("sizeof(src): %lu", sizeof(src));
|
||||
g_debug("width*height*stride: %d", width * height * src_stride);
|
||||
g_assert(src_height >= height);
|
||||
g_assert(src_width >= width);
|
||||
|
||||
dst = cairo_image_surface_get_data(tmp);
|
||||
dst_stride = cairo_image_surface_get_stride(tmp);
|
||||
dest_surface = cairo_image_surface_create(src_format, src_width, src_height);
|
||||
tmp_surface = cairo_image_surface_create(src_format, src_width, src_height);
|
||||
|
||||
a = 0;
|
||||
for (i = 0; i < size; i++) {
|
||||
double f = i - half;
|
||||
a += kernel[i] = exp(-f * f / bluriness) * 80;
|
||||
if (cairo_surface_status(dest_surface) || cairo_surface_status(tmp_surface)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int start_x = fmax(fmin(from.x, to.x), 0);
|
||||
int start_y = fmax(fmin(from.y, to.y), 0);
|
||||
cr = cairo_create(tmp_surface);
|
||||
cairo_set_source_surface(cr, surface, 0, 0);
|
||||
cairo_paint(cr);
|
||||
cairo_destroy(cr);
|
||||
|
||||
int max_x = fmin(fmax(from.x, to.x), width);
|
||||
int max_y = fmin(fmax(from.y, to.y), height);
|
||||
cr = cairo_create(dest_surface);
|
||||
cairo_set_source_surface(cr, surface, 0, 0);
|
||||
cairo_paint(cr);
|
||||
cairo_destroy(cr);
|
||||
|
||||
for (i = 0; i < height; i++) {
|
||||
s = (uint32_t *)(src + i * src_stride);
|
||||
d = (uint32_t *)(dst + i * dst_stride);
|
||||
for (j = 0; j < width; j++) {
|
||||
d[j] = s[j];
|
||||
}
|
||||
}
|
||||
dst = cairo_image_surface_get_data(dest_surface);
|
||||
tmp = cairo_image_surface_get_data(tmp_surface);
|
||||
dst_stride = cairo_image_surface_get_stride(dest_surface);
|
||||
|
||||
struct gaussian_kernel *gaussian = gaussian_kernel(4, 3.1);
|
||||
|
||||
int start_x = CLAMP(x, 0, src_width);
|
||||
int start_y = CLAMP(y - offset_y, 0, src_height);
|
||||
|
||||
int end_x = CLAMP(x + width, 0, src_width);
|
||||
int end_y = CLAMP(y + height + offset_y, 0, src_height);
|
||||
|
||||
sum = (guint)gaussian->sum;
|
||||
|
||||
/* Horizontally blur from surface -> tmp */
|
||||
for (i = start_y; i < max_y; i++) {
|
||||
for (i = start_y; i < end_y; i++) {
|
||||
s = (uint32_t *)(src + i * src_stride);
|
||||
d = (uint32_t *)(dst + i * dst_stride);
|
||||
for (j = start_x; j < max_x; j++) {
|
||||
x = y = z = w = 0;
|
||||
for (k = 0; k < size; k++) {
|
||||
if (j - half + k < 0 || j - half + k >= width) continue;
|
||||
d = (uint32_t *)(tmp + i * dst_stride);
|
||||
for (j = start_x; j < end_x; j++) {
|
||||
u = v = w = z = 0;
|
||||
for (k = 0; k < gaussian->size; k++) {
|
||||
gdouble multiplier = gaussian->kernel[k];
|
||||
|
||||
if (j - half + k < 0 || j - half + k >= src_width) {
|
||||
continue;
|
||||
}
|
||||
|
||||
p = s[j - half + k];
|
||||
|
||||
x += ((p >> 24) & 0xff) * kernel[k];
|
||||
y += ((p >> 16) & 0xff) * kernel[k];
|
||||
z += ((p >> 8) & 0xff) * kernel[k];
|
||||
w += ((p >> 0) & 0xff) * kernel[k];
|
||||
u += ((p >> 24) & 0xff) * multiplier;
|
||||
v += ((p >> 16) & 0xff) * multiplier;
|
||||
w += ((p >> 8) & 0xff) * multiplier;
|
||||
z += ((p >> 0) & 0xff) * multiplier;
|
||||
}
|
||||
d[j] = (x / a << 24) | (y / a << 16) | (z / a << 8) | w / a;
|
||||
|
||||
d[j] = (u / sum << 24) | (v / sum << 16) | (w / sum << 8) | z / sum;
|
||||
}
|
||||
}
|
||||
|
||||
/* Then vertically blur from tmp -> surface */
|
||||
for (i = start_y; i < max_y; i++) {
|
||||
s = (uint32_t *)(dst + i * dst_stride);
|
||||
d = (uint32_t *)(src + i * src_stride);
|
||||
for (j = start_x; j < max_x; j++) {
|
||||
x = y = z = w = 0;
|
||||
for (k = 0; k < size; k++) {
|
||||
if (i - half + k < 0 || i - half + k >= height) {
|
||||
for (i = start_y; i < end_y; i++) {
|
||||
d = (uint32_t *)(dst + i * dst_stride);
|
||||
for (j = start_x; j < end_x; j++) {
|
||||
u = v = w = z = 0;
|
||||
for (k = 0; k < gaussian->size; k++) {
|
||||
gdouble multiplier = gaussian->kernel[k];
|
||||
|
||||
if (i - half + k < 0 || i - half + k >= src_height) {
|
||||
continue;
|
||||
}
|
||||
|
||||
s = (uint32_t *)(dst + (i - half + k) * dst_stride);
|
||||
s = (uint32_t *)(tmp + (i - half + k) * dst_stride);
|
||||
p = s[j];
|
||||
|
||||
x += ((p >> 24) & 0xff) * kernel[k];
|
||||
y += ((p >> 16) & 0xff) * kernel[k];
|
||||
z += ((p >> 8) & 0xff) * kernel[k];
|
||||
w += ((p >> 0) & 0xff) * kernel[k];
|
||||
u += ((p >> 24) & 0xff) * multiplier;
|
||||
v += ((p >> 16) & 0xff) * multiplier;
|
||||
w += ((p >> 8) & 0xff) * multiplier;
|
||||
z += ((p >> 0) & 0xff) * multiplier;
|
||||
}
|
||||
d[j] = (x / a << 24) | (y / a << 16) | (z / a << 8) | w / a;
|
||||
|
||||
d[j] = (u / sum << 24) | (v / sum << 16) | (w / sum << 8) | z / sum;
|
||||
}
|
||||
}
|
||||
|
||||
cairo_surface_destroy(tmp);
|
||||
cairo_surface_mark_dirty(surface);
|
||||
// Mark destination surface as dirty since it was altered with custom data.
|
||||
cairo_surface_mark_dirty(dest_surface);
|
||||
cairo_surface_t *final =
|
||||
cairo_image_surface_create(src_format, (int)width, (int)height);
|
||||
cr = cairo_create(final);
|
||||
cairo_set_source_surface(cr, dest_surface, -x, -y);
|
||||
cairo_paint(cr);
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(dest_surface);
|
||||
cairo_surface_destroy(tmp_surface);
|
||||
gaussian_kernel_free(gaussian);
|
||||
return final;
|
||||
}
|
||||
|
||||
static void convert_pango_rectangle_to_swappy_box(pango_rectangle_t rectangle,
|
||||
@ -227,7 +222,7 @@ static void render_shape_arrow(cairo_t *cr, struct swappy_paint_shape shape) {
|
||||
double r = 20;
|
||||
double scaling_factor = shape.w / 4;
|
||||
|
||||
double alpha = M_PI / 6;
|
||||
double alpha = G_PI / 6;
|
||||
double ta = 5 * alpha;
|
||||
double tb = 7 * alpha;
|
||||
double xa = r * cos(ta);
|
||||
@ -284,7 +279,7 @@ static void render_shape_ellipse(cairo_t *cr, struct swappy_paint_shape shape) {
|
||||
cairo_get_matrix(cr, &save_matrix);
|
||||
cairo_translate(cr, xc, yc);
|
||||
cairo_scale(cr, x / n, y / n);
|
||||
cairo_arc(cr, 0, 0, r, 0, 2 * M_PI);
|
||||
cairo_arc(cr, 0, 0, r, 0, 2 * G_PI);
|
||||
cairo_set_matrix(cr, &save_matrix);
|
||||
cairo_stroke(cr);
|
||||
cairo_close_path(cr);
|
||||
@ -344,14 +339,37 @@ static void render_buffers(cairo_t *cr, struct swappy_state *state) {
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
static void render_background(cairo_t *cr) {
|
||||
static void render_background(cairo_t *cr, struct swappy_state *state) {
|
||||
cairo_set_source_rgb(cr, 0, 0, 0);
|
||||
cairo_paint(cr);
|
||||
}
|
||||
|
||||
static void render_blur(cairo_t *cr, struct swappy_paint_blur blur,
|
||||
bool is_committed, gint scaling_factor) {
|
||||
if (!is_committed) {
|
||||
static void render_blur(cairo_t *cr, struct swappy_paint *paint,
|
||||
gint scaling_factor) {
|
||||
struct swappy_paint_blur blur = paint->content.blur;
|
||||
|
||||
cairo_surface_t *target = cairo_get_target(cr);
|
||||
|
||||
double x = MIN(blur.from.x, blur.to.x);
|
||||
double y = MIN(blur.from.y, blur.to.y);
|
||||
double w = ABS(blur.from.x - blur.to.x);
|
||||
double h = ABS(blur.from.y - blur.to.y);
|
||||
|
||||
cairo_save(cr);
|
||||
|
||||
if (!paint->is_committed) {
|
||||
cairo_surface_t *blurred =
|
||||
blur_surface(target, x, y, w, h, blur.blur_level);
|
||||
|
||||
if (blurred && cairo_surface_status(blurred) == CAIRO_STATUS_SUCCESS) {
|
||||
cairo_set_source_surface(cr, blurred, x, y);
|
||||
cairo_paint(cr);
|
||||
if (blur.surface) {
|
||||
cairo_surface_destroy(blur.surface);
|
||||
}
|
||||
paint->content.blur.surface = blurred;
|
||||
}
|
||||
|
||||
// Blur not committed yet, draw bounding rectangle
|
||||
struct swappy_paint_shape rect = {
|
||||
.r = 0,
|
||||
@ -364,8 +382,16 @@ static void render_blur(cairo_t *cr, struct swappy_paint_blur blur,
|
||||
.type = SWAPPY_PAINT_MODE_RECTANGLE,
|
||||
};
|
||||
render_shape_rectangle(cr, rect);
|
||||
|
||||
} else {
|
||||
cairo_surface_t *surface = blur.surface;
|
||||
if (surface && cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) {
|
||||
cairo_set_source_surface(cr, surface, x, y);
|
||||
cairo_paint(cr);
|
||||
}
|
||||
blur_paint(cr, &blur, scaling_factor);
|
||||
}
|
||||
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
static void render_brush(cairo_t *cr, struct swappy_paint_brush brush) {
|
||||
@ -395,7 +421,7 @@ static void render_paint(cairo_t *cr, struct swappy_paint *paint,
|
||||
}
|
||||
switch (paint->type) {
|
||||
case SWAPPY_PAINT_MODE_BLUR:
|
||||
render_blur(cr, paint->content.blur, paint->is_committed, scaling_factor);
|
||||
render_blur(cr, paint, scaling_factor);
|
||||
break;
|
||||
case SWAPPY_PAINT_MODE_BRUSH:
|
||||
render_brush(cr, paint->content.brush);
|
||||
@ -426,14 +452,15 @@ static void render_paints(cairo_t *cr, struct swappy_state *state) {
|
||||
}
|
||||
|
||||
void render_state(struct swappy_state *state) {
|
||||
cairo_t *cr = cairo_create(state->cairo_surface);
|
||||
cairo_surface_t *surface = state->cairo_surface;
|
||||
cairo_t *cr = cairo_create(surface);
|
||||
|
||||
render_background(cr);
|
||||
render_background(cr, state);
|
||||
render_buffers(cr, state);
|
||||
render_paints(cr, state);
|
||||
|
||||
cairo_destroy(cr);
|
||||
|
||||
// Drawing is finished, notify the GtkDrawingArea it needs to be redrawn.
|
||||
gtk_widget_queue_draw(state->ui->area);
|
||||
|
||||
cairo_destroy(cr);
|
||||
}
|
||||
|
10
src/util.c
10
src/util.c
@ -1,4 +1,3 @@
|
||||
|
||||
#include "util.h"
|
||||
|
||||
#include <glib.h>
|
||||
@ -37,3 +36,12 @@ gchar *string_insert_chars_at(gchar *str, gchar *chars, size_t pos) {
|
||||
|
||||
return new_str;
|
||||
}
|
||||
|
||||
void pixel_data_print(guint32 pixel) {
|
||||
const guint32 r = pixel >> 24 & 0xff;
|
||||
const guint32 g = pixel >> 16 & 0xff;
|
||||
const guint32 b = pixel >> 8 & 0xff;
|
||||
const guint32 a = pixel >> 0 & 0xff;
|
||||
|
||||
g_debug("rgba(%u, %d, %u, %u)", r, g, b, a);
|
||||
}
|
||||
|
BIN
test/images/passwords.png
Normal file
BIN
test/images/passwords.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
Loading…
Reference in New Issue
Block a user