/*
 * Copyright (C) 2009 Canonical Ltd.
 * Author: Robert Ancell <robert.ancell@canonical.com>
 * 
 * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
 * license.
 */

#include <string.h>
#include "page.h"


enum {
    IMAGE_CHANGED,
    SIZE_CHANGED,
    SCAN_LINE_CHANGED,
    ORIENTATION_CHANGED,
    CROP_CHANGED,
    LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0, };

struct PagePrivate
{
    /* Resolution of page */
    gint dpi;

    /* Number of rows in this page or -1 if currently unknown */
    gint rows;

    /* Color profile */
    gchar *color_profile;

    /* Scanned image data */
    GdkPixbuf *image;

    /* Page is getting data */
    gboolean scanning;

    /* TRUE if have some page data */
    gboolean has_data;

    /* Expected next scan row */
    gint scan_line;

    /* Rotation of scanned data */
    Orientation orientation;

    /* Crop */
    gboolean has_crop;
    gchar *crop_name;
    gint crop_x, crop_y, crop_width, crop_height;
};

G_DEFINE_TYPE (Page, page, G_TYPE_OBJECT);


Page *
page_new ()
{
    return g_object_new (PAGE_TYPE, NULL);
}


void
page_setup (Page *page, gint width, gint height, gint dpi, Orientation orientation)
{ 
    page->priv->orientation = orientation;
    page->priv->dpi = dpi;
    if (orientation == LEFT_TO_RIGHT || orientation == RIGHT_TO_LEFT)
        page->priv->rows = width;
    else
        page->priv->rows = height;
    page->priv->image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE,
                                        8,
                                        width,
                                        height);
    g_return_if_fail (page->priv->image != NULL);
    gdk_pixbuf_fill (page->priv->image, 0xFFFFFFFF);
}


void
page_set_scan_area (Page *page, gint width, gint rows, gint dpi)
{
    gint height;

    g_return_if_fail (page != NULL);

    /* Variable height, try 50% of the width for now */
    if (rows < 0)
        height = width / 2;
    else
        height = rows;

    /* Rotate page */
    if (page->priv->orientation == LEFT_TO_RIGHT || page->priv->orientation == RIGHT_TO_LEFT) {
        gint t;
        t = width;
        width = height;
        height = t;
    }

    page->priv->rows = rows;
    page->priv->dpi = dpi;

    /* Create a white page */
    /* NOTE: Pixbuf only supports 8 bit RGB images */
    if (page->priv->image)
        g_object_unref (page->priv->image);
    page->priv->image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE,
                                        8,
                                        width,
                                        height);
    g_return_if_fail (page->priv->image != NULL);

    gdk_pixbuf_fill (page->priv->image, 0xFFFFFFFF);
    g_signal_emit (page, signals[SIZE_CHANGED], 0);
    g_signal_emit (page, signals[IMAGE_CHANGED], 0);
}


void
page_start (Page *page)
{
    g_return_if_fail (page != NULL);

    page->priv->scanning = TRUE;
    g_signal_emit (page, signals[SCAN_LINE_CHANGED], 0);
}


gboolean page_is_scanning (Page *page)
{
    g_return_val_if_fail (page != NULL, FALSE);
  
    return page->priv->scanning;
}


static gint
get_sample (guchar *data, gint depth, gint index)
{
    gint i, offset, value, n_bits;

    /* Optimise if using 8 bit samples */
    if (depth == 8)
        return data[index];

    /* Bit offset for this sample */
    offset = depth * index;

    /* Get the remaining bits in the octet this sample starts in */
    i = offset / 8;
    n_bits = 8 - offset % 8;
    value = data[i] & (0xFF >> (8 - n_bits));
    
    /* Add additional octets until get enough bits */
    while (n_bits < depth) {
        value = value << 8 | data[i++];
        n_bits += 8;
    }

    /* Trim remaining bits off */
    if (n_bits > depth)
        value >>= n_bits - depth;

    return value;
}


gboolean page_has_data (Page *page)
{
    g_return_val_if_fail (page != NULL, FALSE);
    return page->priv->has_data;
}


gint page_get_scan_line (Page *page)
{
    g_return_val_if_fail (page != NULL, -1);
    return page->priv->scan_line;
}


static void
set_pixel (ScanLine *line, gint n, gint x, guchar *pixel)
{
    gint sample;
    guchar *data;
  
    data = line->data + line->data_length * n;

    switch (line->format) {
    case LINE_RGB:
        pixel[0] = get_sample (data, line->depth, x*3) * 0xFF / ((1 << line->depth) - 1);
        pixel[1] = get_sample (data, line->depth, x*3+1) * 0xFF / ((1 << line->depth) - 1);
        pixel[2] = get_sample (data, line->depth, x*3+2) * 0xFF / ((1 << line->depth) - 1);
        break;
    case LINE_GRAY:
        /* Bitmap, 0 = white, 1 = black */
        sample = get_sample (data, line->depth, x) * 0xFF / ((1 << line->depth) - 1);
        if (line->depth == 1)
            sample = sample ? 0x00 : 0xFF;

        pixel[0] = pixel[1] = pixel[2] = sample;
        break;
    case LINE_RED:
        pixel[0] = get_sample (data, line->depth, x) * 0xFF / ((1 << line->depth) - 1);
        break;
    case LINE_GREEN:
        pixel[1] = get_sample (data, line->depth, x) * 0xFF / ((1 << line->depth) - 1);
        break;
    case LINE_BLUE:
        pixel[2] = get_sample (data, line->depth, x) * 0xFF / ((1 << line->depth) - 1);
        break;
    }
}


static void
parse_line (Page *page, ScanLine *line, gint n, gboolean *size_changed)
{
    guchar *pixels;
    gint line_number;
    gint i, x = 0, y = 0, x_step = 0, y_step = 0;
    gint rowstride, n_channels;

    line_number = line->number + n;

    /* Extend image if necessary */
    while (line_number >= page_get_scan_height (page)) {
        GdkPixbuf *image;
        gint height, width, new_width, new_height;

        /* Extend image */
        new_width = width = gdk_pixbuf_get_width (page->priv->image);
        new_height = height = gdk_pixbuf_get_height (page->priv->image);
        if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == BOTTOM_TO_TOP) {
            new_height = height + width / 2;
            g_debug("Extending image height from %d pixels to %d pixels", height, new_height);
        }
        else {
            new_width = width + height / 2;
            g_debug("Extending image width from %d pixels to %d pixels", width, new_width);
        }
        image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE,
                                8, new_width, new_height);

        /* Copy old data */
        gdk_pixbuf_fill (page->priv->image, 0xFFFFFFFF);
        if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == LEFT_TO_RIGHT)
            gdk_pixbuf_copy_area (page->priv->image, 0, 0, width, height,
                                  image, 0, 0);
        else
            gdk_pixbuf_copy_area (page->priv->image, 0, 0, width, height,
                                  image, new_width - width, new_height - height);

        g_object_unref (page->priv->image);
        page->priv->image = image;

        *size_changed = TRUE;
    }
  
    switch (page->priv->orientation) {
    case TOP_TO_BOTTOM:
        x = 0;
        y = line_number;
        x_step = 1;
        y_step = 0;
        break;
    case BOTTOM_TO_TOP:
        x = page_get_width (page) - 1;
        y = page_get_height (page) - line_number - 1;
        x_step = -1;
        y_step = 0;
        break;
    case LEFT_TO_RIGHT:
        x = line_number;
        y = page_get_height (page) - 1;
        x_step = 0;
        y_step = -1;
        break;
    case RIGHT_TO_LEFT:
        x = page_get_width (page) - line_number - 1;
        y = 0;
        x_step = 0;
        y_step = 1;
        break;
    }
    pixels = gdk_pixbuf_get_pixels (page->priv->image);
    rowstride = gdk_pixbuf_get_rowstride (page->priv->image);
    n_channels = gdk_pixbuf_get_n_channels (page->priv->image);
    for (i = 0; i < line->width; i++) {
        guchar *pixel;

        pixel = pixels + y * rowstride + x * n_channels;
        set_pixel (line, n, i, pixel);
        x += x_step;
        y += y_step;
    }

    page->priv->scan_line = line_number;
}


void
page_parse_scan_line (Page *page, ScanLine *line)
{
    gint i;
    gboolean size_changed = FALSE;

    g_return_if_fail (page != NULL);

    for (i = 0; i < line->n_lines; i++)
        parse_line (page, line, i, &size_changed);

    page->priv->has_data = TRUE;

    if (size_changed)
        g_signal_emit (page, signals[SIZE_CHANGED], 0);
    g_signal_emit (page, signals[SCAN_LINE_CHANGED], 0);
    g_signal_emit (page, signals[IMAGE_CHANGED], 0);
}


void
page_finish (Page *page)
{
    gboolean size_changed = FALSE;

    g_return_if_fail (page != NULL);

    /* Trim page */
    if (page->priv->rows < 0 &&
        page->priv->scan_line != gdk_pixbuf_get_height (page->priv->image)) {
        GdkPixbuf *image;
        gint width, height, new_width, new_height;

        new_width = width = gdk_pixbuf_get_width (page->priv->image);
        new_height = height = gdk_pixbuf_get_height (page->priv->image);
        if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == BOTTOM_TO_TOP) {
            new_height = page->priv->scan_line;
            g_debug("Trimming image height from %d pixels to %d pixels", height, new_height);
        }
        else {
            new_width = page->priv->scan_line;
            g_debug("Trimming image width from %d pixels to %d pixels", width, new_width);          
        }
        image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE,
                                8,
                                new_width, new_height);

        /* Copy old data */
        if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == LEFT_TO_RIGHT)
            gdk_pixbuf_copy_area (page->priv->image, 0, 0, width, height,
                                  image, 0, 0);
        else
            gdk_pixbuf_copy_area (page->priv->image, width - new_width, height - new_height, width, height,
                                  image, 0, 0);

        g_object_unref (page->priv->image);
        page->priv->image = image;
        size_changed = TRUE;
    }
    page->priv->scanning = FALSE;

    if (size_changed)
        g_signal_emit (page, signals[SIZE_CHANGED], 0);
    g_signal_emit (page, signals[SCAN_LINE_CHANGED], 0);
}


Orientation
page_get_orientation (Page *page)
{
    g_return_val_if_fail (page != NULL, TOP_TO_BOTTOM);

    return page->priv->orientation;
}


void
page_set_orientation (Page *page, Orientation orientation)
{
    gint left_steps, t;
    GdkPixbuf *image;
    gboolean size_changed = FALSE;
    gint width, height;

    g_return_if_fail (page != NULL);

    if (page->priv->orientation == orientation)
        return;

    /* Work out how many times it has been rotated to the left */
    left_steps = orientation - page->priv->orientation;
    if (left_steps < 0)
        left_steps += 4;
  
    width = page_get_width (page);
    height = page_get_height (page);
  
    /* Rotate image */
    if (left_steps == 1)
        image = gdk_pixbuf_rotate_simple (page->priv->image, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
    else if (left_steps == 2)
        image = gdk_pixbuf_rotate_simple (page->priv->image, GDK_PIXBUF_ROTATE_UPSIDEDOWN);
    else
        image = gdk_pixbuf_rotate_simple (page->priv->image, GDK_PIXBUF_ROTATE_CLOCKWISE);
    g_object_unref (page->priv->image);
    page->priv->image = image;
    if (left_steps != 2)
        size_changed = TRUE;

    /* Rotate crop */
    if (page->priv->has_crop) {
        switch (left_steps) {
        /* 90 degrees counter-clockwise */
        case 1:
            t = page->priv->crop_x;
            page->priv->crop_x = page->priv->crop_y;
            page->priv->crop_y = width - (t + page->priv->crop_width);
            t = page->priv->crop_width;
            page->priv->crop_width = page->priv->crop_height;
            page->priv->crop_height = t;
            break;
        /* 180 degrees */
        case 2:
            page->priv->crop_x = width - (page->priv->crop_x + page->priv->crop_width);
            page->priv->crop_y = width - (page->priv->crop_y + page->priv->crop_height);
            break;
        /* 90 degrees clockwise */
        case 3:
            t = page->priv->crop_y;
            page->priv->crop_y = page->priv->crop_x;
            page->priv->crop_x = height - (t + page->priv->crop_height);
            t = page->priv->crop_width;
            page->priv->crop_width = page->priv->crop_height;
            page->priv->crop_height = t;
            break;
        }
    }

    page->priv->orientation = orientation;
    if (size_changed)
        g_signal_emit (page, signals[SIZE_CHANGED], 0);
    g_signal_emit (page, signals[IMAGE_CHANGED], 0);
    g_signal_emit (page, signals[ORIENTATION_CHANGED], 0);
    g_signal_emit (page, signals[CROP_CHANGED], 0);
}


void
page_rotate_left (Page *page)
{
    Orientation orientation;

    g_return_if_fail (page != NULL);

    orientation = page_get_orientation (page);
    if (orientation == RIGHT_TO_LEFT)
        orientation = TOP_TO_BOTTOM;
    else
        orientation++;
    page_set_orientation (page, orientation);
}


void
page_rotate_right (Page *page)
{
    Orientation orientation;

    orientation = page_get_orientation (page);
    if (orientation == TOP_TO_BOTTOM)
        orientation = RIGHT_TO_LEFT;
    else
        orientation--;
    page_set_orientation (page, orientation);
}


gint
page_get_dpi (Page *page)
{
    g_return_val_if_fail (page != NULL, 0);

    return page->priv->dpi;
}


gboolean
page_is_landscape (Page *page)
{
   return page_get_width (page) > page_get_height (page);
}


gint
page_get_width (Page *page)
{
    g_return_val_if_fail (page != NULL, 0);
    return gdk_pixbuf_get_width (page->priv->image);
}


gint
page_get_height (Page *page)
{
    g_return_val_if_fail (page != NULL, 0);
    return gdk_pixbuf_get_height (page->priv->image);
}


gint
page_get_scan_width (Page *page)
{
    g_return_val_if_fail (page != NULL, 0);

    if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == BOTTOM_TO_TOP)
        return gdk_pixbuf_get_width (page->priv->image);
    else
        return gdk_pixbuf_get_height (page->priv->image);
}


gint
page_get_scan_height (Page *page)
{
    g_return_val_if_fail (page != NULL, 0);

    if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == BOTTOM_TO_TOP)
        return gdk_pixbuf_get_height (page->priv->image);
    else
        return gdk_pixbuf_get_width (page->priv->image);  
}


void page_set_color_profile (Page *page, const gchar *color_profile)
{
     g_free (page->priv->color_profile);
     page->priv->color_profile = g_strdup (color_profile);
}


const gchar *page_get_color_profile (Page *page)
{
     return page->priv->color_profile;
}


void
page_set_no_crop (Page *page)
{
    g_return_if_fail (page != NULL);

    if (!page->priv->has_crop)
        return;
    page->priv->has_crop = FALSE;
    g_signal_emit (page, signals[CROP_CHANGED], 0);
}


void
page_set_custom_crop (Page *page, gint width, gint height)
{
    //gint pw, ph;

    g_return_if_fail (page != NULL);
    g_return_if_fail (width >= 1);
    g_return_if_fail (height >= 1);
    
    if (!page->priv->crop_name &&
        page->priv->has_crop &&
        page->priv->crop_width == width &&
        page->priv->crop_height == height)
        return;
    g_free (page->priv->crop_name);
    page->priv->crop_name = NULL;
    page->priv->has_crop = TRUE;

    page->priv->crop_width = width;
    page->priv->crop_height = height;

    /*pw = page_get_width (page);
    ph = page_get_height (page);
    if (page->priv->crop_width < pw)
        page->priv->crop_x = (pw - page->priv->crop_width) / 2;
    else
        page->priv->crop_x = 0;
    if (page->priv->crop_height < ph)
        page->priv->crop_y = (ph - page->priv->crop_height) / 2;
    else
        page->priv->crop_y = 0;*/
    
    g_signal_emit (page, signals[CROP_CHANGED], 0);
}


void
page_set_named_crop (Page *page, const gchar *name)
{
    struct {
        const gchar *name;
        /* Width and height in inches */
        gdouble width, height;
    } named_crops[] =
    {
        {"A4", 8.3, 11.7},
        {"A5", 5.8, 8.3},
        {"A6", 4.1, 5.8},
        {"letter", 8.5, 11},
        {"legal", 8.5, 14},
        {"4x6", 4, 6},
        {NULL, 0, 0}
    };
    gint i;
    gint pw, ph;
    double width, height;

    g_return_if_fail (page != NULL);
    
    for (i = 0; named_crops[i].name && strcmp (name, named_crops[i].name) != 0; i++);
    width = named_crops[i].width;
    height = named_crops[i].height;

    if (!named_crops[i].name) {
        g_warning ("Unknown paper size '%s'", name);
        return;
    }

    g_free (page->priv->crop_name);
    page->priv->crop_name = g_strdup (name);
    page->priv->has_crop = TRUE;
    
    pw = page_get_width (page);
    ph = page_get_height (page);
   
    /* Rotate to match original aspect */
    if (pw > ph) {
        double t;
        t = width;
        width = height;
        height = t;
    }

    /* Custom crop, make slightly smaller than original */
    page->priv->crop_width = (int) (width * page->priv->dpi + 0.5);
    page->priv->crop_height = (int) (height * page->priv->dpi + 0.5);
        
    if (page->priv->crop_width < pw)
        page->priv->crop_x = (pw - page->priv->crop_width) / 2;
    else
        page->priv->crop_x = 0;
    if (page->priv->crop_height < ph)
        page->priv->crop_y = (ph - page->priv->crop_height) / 2;
    else
        page->priv->crop_y = 0;
    g_signal_emit (page, signals[CROP_CHANGED], 0);
}


void
page_move_crop (Page *page, gint x, gint y)
{
    g_return_if_fail (x >= 0);
    g_return_if_fail (y >= 0);
    g_return_if_fail (x < page_get_width (page));
    g_return_if_fail (y < page_get_height (page));

    page->priv->crop_x = x;
    page->priv->crop_y = y;
    g_signal_emit (page, signals[CROP_CHANGED], 0);    
}


void
page_rotate_crop (Page *page)
{
    gint t;
    
    g_return_if_fail (page != NULL);
  
    if (!page->priv->has_crop)
        return;

    t = page->priv->crop_width;
    page->priv->crop_width = page->priv->crop_height;
    page->priv->crop_height = t;
  
    /* Clip custom crops */
    if (!page->priv->crop_name) {
        gint w, h;

        w = page_get_width (page);
        h = page_get_height (page);
        
        if (page->priv->crop_x + page->priv->crop_width > w)
            page->priv->crop_x = w - page->priv->crop_width;
        if (page->priv->crop_x < 0) {
            page->priv->crop_x = 0;
            page->priv->crop_width = w;
        }
        if (page->priv->crop_y + page->priv->crop_height > h)
            page->priv->crop_y = h - page->priv->crop_height;
        if (page->priv->crop_y < 0) {
            page->priv->crop_y = 0;
            page->priv->crop_height = h;
        }
    }

    g_signal_emit (page, signals[CROP_CHANGED], 0);
}


gboolean
page_has_crop (Page *page)
{
    g_return_val_if_fail (page != NULL, FALSE);
    return page->priv->has_crop;
}


void
page_get_crop (Page *page, gint *x, gint *y, gint *width, gint *height)
{
    g_return_if_fail (page != NULL);

    if (x)
        *x = page->priv->crop_x;
    if (y)
        *y = page->priv->crop_y;
    if (width)
        *width = page->priv->crop_width;
    if (height)
        *height = page->priv->crop_height;
}


gchar *
page_get_named_crop (Page *page)
{
    g_return_val_if_fail (page != NULL, NULL);

    if (page->priv->crop_name)
        return g_strdup (page->priv->crop_name);
    else
        return NULL;
}


GdkPixbuf *
page_get_image (Page *page)
{
    g_return_val_if_fail (page != NULL, NULL);
    return g_object_ref (page->priv->image);
}


GdkPixbuf *
page_get_cropped_image (Page *page)
{
    GdkPixbuf *image, *cropped_image;
    gint x, y, w, h, pw, ph;

    g_return_val_if_fail (page != NULL, NULL);

    image = page_get_image (page);

    if (!page->priv->has_crop)
        return image;

    x = page->priv->crop_x;
    y = page->priv->crop_y;
    w = page->priv->crop_width;
    h = page->priv->crop_height;
    pw = gdk_pixbuf_get_width (image);
    ph = gdk_pixbuf_get_height (image);

    /* Trim crop */
    if (x + w >= pw)
        w = pw - x;
    if (y + h >= ph)
        h = ph - y;

    cropped_image = gdk_pixbuf_new_subpixbuf (image, x, y, w, h);
    g_object_unref (image);

    return cropped_image;
}


static gboolean
write_pixbuf_data (const gchar *buf, gsize count, GError **error, GFileOutputStream *stream)
{
    return g_output_stream_write_all (G_OUTPUT_STREAM (stream), buf, count, NULL, NULL, error);
}


static gchar *
get_icc_data_encoded (const gchar *icc_profile_filename)
{
    gchar *contents = NULL;
    gchar *contents_encode = NULL;
    gsize length;
    gboolean ret;
    GError *error = NULL;

    /* Get binary data */
    ret = g_file_get_contents (icc_profile_filename, &contents, &length, &error);
    if (!ret) {
        g_warning ("failed to get icc profile data: %s", error->message);
        g_error_free (error);
    }
    else {
        /* Encode into base64 */
        contents_encode = g_base64_encode ((const guchar *) contents, length);
    }
  
    g_free (contents);
    return contents_encode;
}


gboolean
page_save (Page *page, const gchar *type, GFile *file, GError **error)
{
    GFileOutputStream *stream;
    GdkPixbuf *image;
    gboolean result = FALSE;
    gchar *icc_profile_data = NULL;

    stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error);
    if (!stream)
        return FALSE;

    image = page_get_cropped_image (page);

    if (page->priv->color_profile != NULL)
        icc_profile_data = get_icc_data_encoded (page->priv->color_profile);

    if (strcmp (type, "jpeg") == 0) {
        /* ICC profile is awaiting review in gtk2+ bugzilla */
        gchar *keys[] = { "quality", /* "icc-profile", */ NULL };
        gchar *values[] = { "90", /* icc_profile_data, */ NULL };
        result = gdk_pixbuf_save_to_callbackv (image,
                                               (GdkPixbufSaveFunc) write_pixbuf_data, stream,
                                               "jpeg", keys, values, error);
    }
    else if (strcmp (type, "png") == 0) {
        gchar *keys[] = { "icc-profile", NULL };
        gchar *values[] = { icc_profile_data, NULL };
        if (icc_profile_data == NULL)
            keys[0] = NULL;
        result = gdk_pixbuf_save_to_callbackv (image,
                                               (GdkPixbufSaveFunc) write_pixbuf_data, stream,
                                               "png", keys, values, error);
    }
    else if (strcmp (type, "tiff") == 0) {
        gchar *keys[] = { "compression", "icc-profile", NULL };
        gchar *values[] = { "8" /* Deflate compression */, icc_profile_data, NULL };
        if (icc_profile_data == NULL)
            keys[1] = NULL;
        result = gdk_pixbuf_save_to_callbackv (image,
                                               (GdkPixbufSaveFunc) write_pixbuf_data, stream,
                                               "tiff", keys, values, error);
    }
    else
        result = FALSE; // FIXME: Set GError

    g_free (icc_profile_data);
    g_object_unref (image);
    g_object_unref (stream);

    return result;
}


static void
page_finalize (GObject *object)
{
    Page *page = PAGE (object);
    if (page->priv->image)
        g_object_unref (page->priv->image);
    page->priv->image = NULL;
    G_OBJECT_CLASS (page_parent_class)->finalize (object);
}


static void
page_class_init (PageClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->finalize = page_finalize;

    signals[IMAGE_CHANGED] =
        g_signal_new ("image-changed",
                      G_TYPE_FROM_CLASS (klass),
                      G_SIGNAL_RUN_LAST,
                      G_STRUCT_OFFSET (PageClass, image_changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE, 0);
    signals[SIZE_CHANGED] =
        g_signal_new ("size-changed",
                      G_TYPE_FROM_CLASS (klass),
                      G_SIGNAL_RUN_LAST,
                      G_STRUCT_OFFSET (PageClass, size_changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE, 0);
    signals[SCAN_LINE_CHANGED] =
        g_signal_new ("scan-line-changed",
                      G_TYPE_FROM_CLASS (klass),
                      G_SIGNAL_RUN_LAST,
                      G_STRUCT_OFFSET (PageClass, scan_line_changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE, 0);
    signals[ORIENTATION_CHANGED] =
        g_signal_new ("orientation-changed",
                      G_TYPE_FROM_CLASS (klass),
                      G_SIGNAL_RUN_LAST,
                      G_STRUCT_OFFSET (PageClass, orientation_changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE, 0);
    signals[CROP_CHANGED] =
        g_signal_new ("crop-changed",
                      G_TYPE_FROM_CLASS (klass),
                      G_SIGNAL_RUN_LAST,
                      G_STRUCT_OFFSET (PageClass, crop_changed),
                      NULL, NULL,
                      g_cclosure_marshal_VOID__VOID,
                      G_TYPE_NONE, 0);

    g_type_class_add_private (klass, sizeof (PagePrivate));
}


static void
page_init (Page *page)
{
    page->priv = G_TYPE_INSTANCE_GET_PRIVATE (page, PAGE_TYPE, PagePrivate);
    page->priv->orientation = TOP_TO_BOTTOM;
}