PIKApp/plug-ins/print/print-preview.c

918 lines
27 KiB
C

/* PIKA - Photo and Image Kooker Application
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
*
* Original copyright, applying to most contents (license remains unchanged):
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <libpika/pika.h>
#include <libpika/pikaui.h>
#include "print-preview.h"
enum
{
OFFSETS_CHANGED,
LAST_SIGNAL
};
#define SIZE_REQUEST 200
struct _PrintPreview
{
GtkEventBox parent_instance;
GdkCursor *cursor;
GtkPageSetup *page;
cairo_surface_t *thumbnail;
gboolean dragging;
gboolean inside;
PikaDrawable *drawable;
gdouble image_offset_x;
gdouble image_offset_y;
gdouble image_offset_x_max;
gdouble image_offset_y_max;
gdouble image_width;
gdouble image_height;
gboolean use_full_page;
/* for mouse drags */
gdouble orig_offset_x;
gdouble orig_offset_y;
gint start_x;
gint start_y;
};
struct _PrintPreviewClass
{
GtkEventBoxClass parent_class;
void (* offsets_changed) (PrintPreview *print_preview,
gint offset_x,
gint offset_y);
};
static void print_preview_finalize (GObject *object);
static void print_preview_realize (GtkWidget *widget);
static void print_preview_unrealize (GtkWidget *widget);
static void print_preview_get_preferred_width (GtkWidget *widget,
gint *minimum_width,
gint *natural_width);
static void print_preview_get_preferred_height (GtkWidget *widget,
gint *minimum_height,
gint *natural_height);
static void print_preview_size_allocate (GtkWidget *widget,
GtkAllocation *allocation);
static gboolean print_preview_draw (GtkWidget *widget,
cairo_t *cr);
static gboolean print_preview_button_press_event (GtkWidget *widget,
GdkEventButton *event);
static gboolean print_preview_button_release_event (GtkWidget *widget,
GdkEventButton *event);
static gboolean print_preview_motion_notify_event (GtkWidget *widget,
GdkEventMotion *event);
static gboolean print_preview_leave_notify_event (GtkWidget *widget,
GdkEventCrossing *event);
static gboolean print_preview_is_inside (PrintPreview *preview,
gdouble x,
gdouble y);
static void print_preview_set_inside (PrintPreview *preview,
gboolean inside);
static gdouble print_preview_get_scale (PrintPreview *preview);
static void print_preview_get_page_size (PrintPreview *preview,
gdouble *paper_width,
gdouble *paper_height);
static void print_preview_get_page_margins (PrintPreview *preview,
gdouble *left_margin,
gdouble *right_margin,
gdouble *top_margin,
gdouble *bottom_margin);
static cairo_surface_t * print_preview_get_thumbnail (PikaDrawable *drawable,
gint width,
gint height);
G_DEFINE_TYPE (PrintPreview, print_preview, GTK_TYPE_EVENT_BOX)
#define parent_class print_preview_parent_class
static guint print_preview_signals[LAST_SIGNAL] = { 0 };
#define g_marshal_value_peek_double(v) (v)->data[0].v_double
static void
marshal_VOID__DOUBLE_DOUBLE (GClosure *closure,
GValue *return_value,
guint n_param_values,
const GValue *param_values,
gpointer invocation_hint,
gpointer marshal_data)
{
typedef void (*GMarshalFunc_VOID__DOUBLE_DOUBLE) (gpointer data1,
gdouble arg_1,
gdouble arg_2,
gpointer data2);
register GMarshalFunc_VOID__DOUBLE_DOUBLE callback;
register GCClosure *cc = (GCClosure*) closure;
register gpointer data1, data2;
g_return_if_fail (n_param_values == 3);
if (G_CCLOSURE_SWAP_DATA (closure))
{
data1 = closure->data;
data2 = g_value_peek_pointer (param_values + 0);
}
else
{
data1 = g_value_peek_pointer (param_values + 0);
data2 = closure->data;
}
callback = (GMarshalFunc_VOID__DOUBLE_DOUBLE) (marshal_data ?
marshal_data : cc->callback);
callback (data1,
g_marshal_value_peek_double (param_values + 1),
g_marshal_value_peek_double (param_values + 2),
data2);
}
static void
print_preview_class_init (PrintPreviewClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
print_preview_signals[OFFSETS_CHANGED] =
g_signal_new ("offsets-changed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET (PrintPreviewClass, offsets_changed),
NULL, NULL,
marshal_VOID__DOUBLE_DOUBLE,
G_TYPE_NONE, 2,
G_TYPE_DOUBLE,
G_TYPE_DOUBLE);
object_class->finalize = print_preview_finalize;
widget_class->realize = print_preview_realize;
widget_class->unrealize = print_preview_unrealize;
widget_class->get_preferred_width = print_preview_get_preferred_width;
widget_class->get_preferred_height = print_preview_get_preferred_height;
widget_class->size_allocate = print_preview_size_allocate;
widget_class->draw = print_preview_draw;
widget_class->button_press_event = print_preview_button_press_event;
widget_class->button_release_event = print_preview_button_release_event;
widget_class->motion_notify_event = print_preview_motion_notify_event;
widget_class->leave_notify_event = print_preview_leave_notify_event;
klass->offsets_changed = NULL;
}
static void
print_preview_init (PrintPreview *preview)
{
gtk_event_box_set_visible_window (GTK_EVENT_BOX (preview), FALSE);
gtk_widget_add_events (GTK_WIDGET (preview),
GDK_BUTTON_PRESS_MASK |
GDK_BUTTON_RELEASE_MASK |
GDK_POINTER_MOTION_MASK);
}
static void
print_preview_finalize (GObject *object)
{
PrintPreview *preview = PRINT_PREVIEW (object);
if (preview->thumbnail)
{
cairo_surface_destroy (preview->thumbnail);
preview->thumbnail = NULL;
}
if (preview->page)
{
g_object_unref (preview->page);
preview->page = NULL;
}
G_OBJECT_CLASS (print_preview_parent_class)->finalize (object);
}
static void
print_preview_realize (GtkWidget *widget)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
GTK_WIDGET_CLASS (print_preview_parent_class)->realize (widget);
preview->cursor = gdk_cursor_new_for_display (gtk_widget_get_display (widget),
GDK_HAND1);
}
static void
print_preview_unrealize (GtkWidget *widget)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
if (preview->cursor)
g_object_unref (preview->cursor);
GTK_WIDGET_CLASS (print_preview_parent_class)->unrealize (widget);
}
static void
print_preview_size_request (GtkWidget *widget,
GtkRequisition *requisition)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
gdouble paper_width;
gdouble paper_height;
gint border;
border = gtk_container_get_border_width (GTK_CONTAINER (widget)) + 1;
print_preview_get_page_size (preview, &paper_width, &paper_height);
if (paper_width > paper_height)
{
requisition->height = SIZE_REQUEST;
requisition->width = paper_width * SIZE_REQUEST / paper_height;
requisition->width = MIN (requisition->width, 2 * SIZE_REQUEST);
}
else
{
requisition->width = SIZE_REQUEST;
requisition->height = paper_height * SIZE_REQUEST / paper_width;
requisition->height = MIN (requisition->height, 2 * SIZE_REQUEST);
}
requisition->width += 2 * border;
requisition->height += 2 * border;
}
static void
print_preview_get_preferred_width (GtkWidget *widget,
gint *minimum_width,
gint *natural_width)
{
GtkRequisition requisition;
print_preview_size_request (widget, &requisition);
*minimum_width = *natural_width = requisition.width;
}
static void
print_preview_get_preferred_height (GtkWidget *widget,
gint *minimum_height,
gint *natural_height)
{
GtkRequisition requisition;
print_preview_size_request (widget, &requisition);
*minimum_height = *natural_height = requisition.height;
}
static void
print_preview_size_allocate (GtkWidget *widget,
GtkAllocation *allocation)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
GTK_WIDGET_CLASS (print_preview_parent_class)->size_allocate (widget,
allocation);
if (preview->thumbnail)
{
cairo_surface_destroy (preview->thumbnail);
preview->thumbnail = NULL;
}
}
static gboolean
print_preview_button_press_event (GtkWidget *widget,
GdkEventButton *event)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
if (event->type == GDK_BUTTON_PRESS && event->button == 1 && preview->inside)
{
GdkDisplay *display = gtk_widget_get_display (widget);
GdkSeat *seat = gdk_display_get_default_seat (display);
GdkCursor *cursor;
cursor = gdk_cursor_new_for_display (gtk_widget_get_display (widget),
GDK_FLEUR);
if (gdk_seat_grab (seat, gdk_event_get_window ((GdkEvent *) event),
GDK_SEAT_CAPABILITY_ALL_POINTING, FALSE,
cursor,
(GdkEvent *) event,
NULL, NULL) == GDK_GRAB_SUCCESS)
{
preview->orig_offset_x = preview->image_offset_x;
preview->orig_offset_y = preview->image_offset_y;
preview->start_x = event->x;
preview->start_y = event->y;
preview->dragging = TRUE;
}
g_object_unref (cursor);
}
return FALSE;
}
static gboolean
print_preview_button_release_event (GtkWidget *widget,
GdkEventButton *event)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
if (preview->dragging)
{
GdkDisplay *display = gtk_widget_get_display (widget);
GdkSeat *seat = gdk_display_get_default_seat (display);
gdk_seat_ungrab (seat);
preview->dragging = FALSE;
print_preview_set_inside (preview,
print_preview_is_inside (preview,
event->x, event->y));
}
return FALSE;
}
static gboolean
print_preview_motion_notify_event (GtkWidget *widget,
GdkEventMotion *event)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
if (preview->dragging)
{
gdouble scale = print_preview_get_scale (preview);
gdouble offset_x;
gdouble offset_y;
offset_x = (preview->orig_offset_x +
(event->x - preview->start_x) / scale);
offset_y = (preview->orig_offset_y +
(event->y - preview->start_y) / scale);
offset_x = CLAMP (offset_x, 0, preview->image_offset_x_max);
offset_y = CLAMP (offset_y, 0, preview->image_offset_y_max);
if (preview->image_offset_x != offset_x ||
preview->image_offset_y != offset_y)
{
print_preview_set_image_offsets (preview, offset_x, offset_y);
g_signal_emit (preview,
print_preview_signals[OFFSETS_CHANGED], 0,
preview->image_offset_x,
preview->image_offset_y);
}
}
else
{
print_preview_set_inside (preview,
print_preview_is_inside (preview,
event->x, event->y));
}
return FALSE;
}
static gboolean
print_preview_leave_notify_event (GtkWidget *widget,
GdkEventCrossing *event)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
if (event->mode == GDK_CROSSING_NORMAL)
print_preview_set_inside (preview, FALSE);
return FALSE;
}
static gboolean
print_preview_draw (GtkWidget *widget,
cairo_t *cr)
{
PrintPreview *preview = PRINT_PREVIEW (widget);
GtkAllocation allocation;
GdkRGBA color;
gdouble paper_width;
gdouble paper_height;
gdouble left_margin;
gdouble right_margin;
gdouble top_margin;
gdouble bottom_margin;
gdouble scale;
gint border;
gtk_widget_get_allocation (widget, &allocation);
border = gtk_container_get_border_width (GTK_CONTAINER (widget)) + 1;
print_preview_get_page_size (preview, &paper_width, &paper_height);
print_preview_get_page_margins (preview,
&left_margin, &right_margin,
&top_margin, &bottom_margin);
scale = print_preview_get_scale (preview);
cairo_translate (cr, border, border);
if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
{
gint width = allocation.width - 2 * border;
cairo_translate (cr, width - scale * paper_width, 0);
}
cairo_set_line_width (cr, 1.0);
/* draw page background */
cairo_rectangle (cr, 0, 0, scale * paper_width, scale * paper_height);
color = (GdkRGBA) { 0.0, 0.0, 0.0, 1.0 };
gdk_cairo_set_source_rgba (cr, &color);
cairo_stroke_preserve (cr);
color = (GdkRGBA) { 1.0, 1.0, 1.0, 1.0 };
gdk_cairo_set_source_rgba (cr, &color);
cairo_fill (cr);
/* draw page_margins */
cairo_rectangle (cr,
scale * left_margin,
scale * top_margin,
scale * (paper_width - left_margin - right_margin),
scale * (paper_height - top_margin - bottom_margin));
color = (GdkRGBA) { 0.0, 0.0, 0.0, 0.3 };
gdk_cairo_set_source_rgba (cr, &color);
cairo_stroke (cr);
cairo_translate (cr,
scale * (left_margin + preview->image_offset_x),
scale * (top_margin + preview->image_offset_y));
if (preview->dragging || preview->inside)
{
cairo_rectangle (cr,
0, 0,
scale * preview->image_width,
scale * preview->image_height);
color = (GdkRGBA) { 0.0, 0.0, 0.0, 1.0 };
gdk_cairo_set_source_rgba (cr, &color);
cairo_stroke (cr);
}
if (preview->thumbnail == NULL &&
pika_item_is_valid (PIKA_ITEM (preview->drawable)))
{
preview->thumbnail =
print_preview_get_thumbnail (preview->drawable,
MIN (allocation.width, 1024),
MIN (allocation.height, 1024));
}
if (preview->thumbnail != NULL)
{
gdouble scale_x;
gdouble scale_y;
scale_x = (preview->image_width /
cairo_image_surface_get_width (preview->thumbnail));
scale_y = (preview->image_height /
cairo_image_surface_get_height (preview->thumbnail));
cairo_rectangle (cr, 0, 0, preview->image_width, preview->image_height);
cairo_scale (cr, scale_x * scale, scale_y * scale);
cairo_set_source_surface (cr, preview->thumbnail, 0, 0);
cairo_fill (cr);
}
return FALSE;
}
/**
* print_preview_new:
* @page: page setup
* @drawable: the drawable to print
*
* Creates a new #PrintPreview widget.
*
* Returns: the new #PrintPreview widget.
**/
GtkWidget *
print_preview_new (GtkPageSetup *page,
PikaDrawable *drawable)
{
PrintPreview *preview;
g_return_val_if_fail (GTK_IS_PAGE_SETUP (page), NULL);
preview = g_object_new (PRINT_TYPE_PREVIEW, NULL);
preview->drawable = drawable;
print_preview_set_page_setup (preview, page);
return GTK_WIDGET (preview);
}
/**
* print_preview_set_image_dpi:
* @preview: a #PrintPreview.
* @xres: the X resolution
* @yres: the Y resolution
*
* Sets the resolution of the image/drawable displayed by the
* #PrintPreview.
**/
void
print_preview_set_image_dpi (PrintPreview *preview,
gdouble xres,
gdouble yres)
{
gdouble width;
gdouble height;
g_return_if_fail (PRINT_IS_PREVIEW (preview));
g_return_if_fail (xres > 0.0 && yres > 0.0);
width = pika_drawable_get_width (preview->drawable) * 72.0 / xres;
height = pika_drawable_get_height (preview->drawable) * 72.0 / yres;
if (width != preview->image_width || height != preview->image_height)
{
preview->image_width = width;
preview->image_height = height;
gtk_widget_queue_draw (GTK_WIDGET (preview));
}
}
/**
* print_preview_set_page_setup:
* @preview: a #PrintPreview.
* @page: the page setup to use
*
* Sets the page setup to use by the #PrintPreview.
**/
void
print_preview_set_page_setup (PrintPreview *preview,
GtkPageSetup *page)
{
g_return_if_fail (PRINT_IS_PREVIEW (preview));
g_return_if_fail (GTK_IS_PAGE_SETUP (page));
if (preview->page)
g_object_unref (preview->page);
preview->page = gtk_page_setup_copy (page);
gtk_widget_queue_resize (GTK_WIDGET (preview));
}
/**
* print_preview_set_image_offsets:
* @preview: a #PrintPreview.
* @offset_x: the X offset
* @offset_y: the Y offset
*
* Sets the offsets of the image/drawable displayed by the #PrintPreview.
* It does not emit the "offsets-changed" signal.
**/
void
print_preview_set_image_offsets (PrintPreview *preview,
gdouble offset_x,
gdouble offset_y)
{
g_return_if_fail (PRINT_IS_PREVIEW (preview));
preview->image_offset_x = offset_x;
preview->image_offset_y = offset_y;
gtk_widget_queue_draw (GTK_WIDGET (preview));
}
/**
* print_preview_set_image_offsets_max:
* @preview: a #PrintPreview.
* @offset_x_max: the maximum X offset allowed
* @offset_y_max: the maximum Y offset allowed
*
* Sets the maximum offsets of the image/drawable displayed by the
* #PrintPreview. It does not emit the "offsets-changed" signal.
**/
void
print_preview_set_image_offsets_max (PrintPreview *preview,
gdouble offset_x_max,
gdouble offset_y_max)
{
g_return_if_fail (PRINT_IS_PREVIEW (preview));
preview->image_offset_x_max = offset_x_max;
preview->image_offset_y_max = offset_y_max;
gtk_widget_queue_draw (GTK_WIDGET (preview));
}
/**
* print_preview_set_use_full_page:
* @preview: a #PrintPreview.
* @full_page: TRUE to ignore the page margins
*
* If @full_page is TRUE, the page margins are ignored and the full page
* can be used to setup printing.
**/
void
print_preview_set_use_full_page (PrintPreview *preview,
gboolean full_page)
{
g_return_if_fail (PRINT_IS_PREVIEW (preview));
preview->use_full_page = full_page;
gtk_widget_queue_draw (GTK_WIDGET (preview));
}
static gboolean
print_preview_is_inside (PrintPreview *preview,
gdouble x,
gdouble y)
{
GtkWidget *widget = GTK_WIDGET (preview);
GtkAllocation allocation;
gdouble left_margin;
gdouble right_margin;
gdouble top_margin;
gdouble bottom_margin;
gdouble scale;
gint border;
gtk_widget_get_allocation (widget, &allocation);
border = gtk_container_get_border_width (GTK_CONTAINER (widget)) + 1;
x -= border;
scale = print_preview_get_scale (preview);
if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
{
gdouble paper_width;
gdouble paper_height;
gint width = allocation.width - 2 * border;
print_preview_get_page_size (preview, &paper_width, &paper_height);
x -= width - scale * paper_width;
}
print_preview_get_page_margins (preview,
&left_margin, &right_margin,
&top_margin, &bottom_margin);
x = x / scale - left_margin;
y = y / scale - top_margin;
return (x > preview->image_offset_x &&
x < preview->image_offset_x + preview->image_width &&
y > preview->image_offset_y &&
y < preview->image_offset_y + preview->image_height);
}
static void
print_preview_set_inside (PrintPreview *preview,
gboolean inside)
{
if (inside != preview->inside)
{
GtkWidget *widget = GTK_WIDGET (preview);
preview->inside = inside;
if (gtk_widget_is_drawable (widget))
gdk_window_set_cursor (gtk_widget_get_window (widget),
inside ? preview->cursor : NULL);
gtk_widget_queue_draw (widget);
}
}
static gdouble
print_preview_get_scale (PrintPreview *preview)
{
GtkWidget *widget = GTK_WIDGET (preview);
GtkAllocation allocation;
gdouble paper_width;
gdouble paper_height;
gdouble scale_x;
gdouble scale_y;
gint border;
gtk_widget_get_allocation (widget, &allocation);
border = gtk_container_get_border_width (GTK_CONTAINER (widget)) + 1;
print_preview_get_page_size (preview, &paper_width, &paper_height);
scale_x = (gdouble) (allocation.width - 2 * border) / paper_width;
scale_y = (gdouble) (allocation.height - 2 * border) / paper_height;
return MIN (scale_x, scale_y);
}
static void
print_preview_get_page_size (PrintPreview *preview,
gdouble *paper_width,
gdouble *paper_height)
{
*paper_width = gtk_page_setup_get_paper_width (preview->page,
GTK_UNIT_POINTS);
*paper_height = gtk_page_setup_get_paper_height (preview->page,
GTK_UNIT_POINTS);
}
static void
print_preview_get_page_margins (PrintPreview *preview,
gdouble *left_margin,
gdouble *right_margin,
gdouble *top_margin,
gdouble *bottom_margin)
{
if (preview->use_full_page)
{
*left_margin = 0.0;
*right_margin = 0.0;
*top_margin = 0.0;
*bottom_margin = 0.0;
}
else
{
*left_margin = gtk_page_setup_get_left_margin (preview->page,
GTK_UNIT_POINTS);
*right_margin = gtk_page_setup_get_right_margin (preview->page,
GTK_UNIT_POINTS);
*top_margin = gtk_page_setup_get_top_margin (preview->page,
GTK_UNIT_POINTS);
*bottom_margin = gtk_page_setup_get_bottom_margin (preview->page,
GTK_UNIT_POINTS);
}
}
/* This thumbnail code should eventually end up in libpikaui. */
static cairo_surface_t *
print_preview_get_thumbnail (PikaDrawable *drawable,
gint width,
gint height)
{
cairo_surface_t *surface;
cairo_format_t format;
GBytes *data;
guchar *dest;
const guchar *src;
gint src_stride;
gint dest_stride;
gint y;
gint bpp;
g_return_val_if_fail (width > 0 && width <= 1024, NULL);
g_return_val_if_fail (height > 0 && height <= 1024, NULL);
data = pika_drawable_get_thumbnail_data (drawable,
width, height,
&width, &height, &bpp);
switch (bpp)
{
case 1:
case 3:
format = CAIRO_FORMAT_RGB24;
break;
case 2:
case 4:
format = CAIRO_FORMAT_ARGB32;
break;
default:
g_assert_not_reached ();
break;
}
surface = cairo_image_surface_create (format, width, height);
src = g_bytes_get_data (data, NULL);
src_stride = width * bpp;
dest = cairo_image_surface_get_data (surface);
dest_stride = cairo_image_surface_get_stride (surface);
for (y = 0; y < height; y++)
{
const guchar *s = src;
guchar *d = dest;
gint w = width;
switch (bpp)
{
case 1:
while (w--)
{
PIKA_CAIRO_RGB24_SET_PIXEL (d, s[0], s[0], s[0]);
s += 1;
d += 4;
}
break;
case 2:
while (w--)
{
PIKA_CAIRO_ARGB32_SET_PIXEL (d, s[0], s[0], s[0], s[1]);
s += 2;
d += 4;
}
break;
case 3:
while (w--)
{
PIKA_CAIRO_RGB24_SET_PIXEL (d, s[0], s[1], s[2]);
s += 3;
d += 4;
}
break;
case 4:
while (w--)
{
PIKA_CAIRO_ARGB32_SET_PIXEL (d, s[0], s[1], s[2], s[3]);
s += 4;
d += 4;
}
break;
}
src += src_stride;
dest += dest_stride;
}
g_bytes_unref (data);
cairo_surface_mark_dirty (surface);
return surface;
}