diff options
-rw-r--r-- | data/de.marvinborner.zathura-note.metainfo.xml | 1 | ||||
-rw-r--r-- | license | 13 | ||||
-rw-r--r-- | meson.build | 5 | ||||
-rw-r--r-- | zathura-note/cairo_jpg.c | 612 | ||||
-rw-r--r-- | zathura-note/cairo_jpg.h | 75 | ||||
-rw-r--r-- | zathura-note/note.c | 244 |
6 files changed, 894 insertions, 56 deletions
diff --git a/data/de.marvinborner.zathura-note.metainfo.xml b/data/de.marvinborner.zathura-note.metainfo.xml index 3ca7771..8958e32 100644 --- a/data/de.marvinborner.zathura-note.metainfo.xml +++ b/data/de.marvinborner.zathura-note.metainfo.xml @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <component type="addon"> <id>de.marvinborner.zathura-note</id> - <project_license>WTFPL</project_license> <extends>org.pwmt.zathura</extends> <name>Zathura-note</name> <summary>Notability .note support for zathura</summary> diff --git a/license b/license deleted file mode 100644 index b01085e..0000000 --- a/license +++ /dev/null @@ -1,13 +0,0 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - -Copyright (C) 2021 Marvin Borner <develop@marvinborner.de> - -Everyone is permitted to copy and distribute verbatim or modified -copies of this license document, and changing it is allowed as long -as the name is changed. - - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/meson.build b/meson.build index 8f7647f..5683db2 100644 --- a/meson.build +++ b/meson.build @@ -21,6 +21,7 @@ glib = dependency('glib-2.0') cairo = dependency('cairo') zip = dependency('libzip') plist = dependency('libplist') +jpeg = dependency('libjpeg') build_dependencies = [ zathura, @@ -28,7 +29,8 @@ build_dependencies = [ glib, cairo, zip, - plist + plist, + jpeg ] plugindir = zathura.get_pkgconfig_variable('plugindir') @@ -53,6 +55,7 @@ flags = [ flags = cc.get_supported_arguments(flags) sources = files( + 'zathura-note/cairo_jpg.c', 'zathura-note/plugin.c', 'zathura-note/note.c' ) diff --git a/zathura-note/cairo_jpg.c b/zathura-note/cairo_jpg.c new file mode 100644 index 0000000..cd47f20 --- /dev/null +++ b/zathura-note/cairo_jpg.c @@ -0,0 +1,612 @@ +/* Copyright 2018 Bernhard R. Fischer, 4096R/8E24F29D <bf@abenteuerland.at> + * + * This file is part of Cairo_JPG. + * + * Cairo_JPG is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Cairo_JPG 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Cairo_JPG. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! \file cairo_jpg.c + * This file contains two functions for reading and writing JPEG files from + * and to Cairo image surfaces. It uses the functions from the libjpeg. + * Most of the code is directly derived from the online example at + * http://libjpeg-turbo.virtualgl.org/Documentation/Documentation + * + * All prototypes are defined in cairo_jpg.h All functions and their parameters + * and return values are described below directly at the functions. You may + * also have a look at the preprocessor macros defined below. + * + * To compile this code you need to have installed the packages libcairo2-dev + * and libjpeg-dev. Compile with the following to create an object file to link + * with your code: + * gcc -std=c99 -Wall -c `pkg-config cairo libjpeg --cflags --libs` cairo_jpg.c + * Use the following command to include the main() function and create an + * executable for testing of this code: + * gcc -std=c99 -Wall -o cairo_jpg -DCAIRO_JPEG_MAIN `pkg-config cairo libjpeg --cflags --libs` cairo_jpg.c + * + * @author Bernhard R. Fischer, 4096R/8E24F29D bf@abenteuerland.at + * @version 2020/01/18 + * @license LGPL3. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include <fcntl.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include <cairo.h> +#include <jpeglib.h> + +#include "cairo_jpg.h" + +/*! Macro to activate main() function. This is only used for testing. Comment + * it out (#undef) if you link this file to your own program. + */ +//#define CAIRO_JPEG_MAIN +// +/*! Define this to use an alternate implementation of + * cairo_image_surface_create_from_jpeg() which fstat(3)s the file before + * reading (see below). For huge files this /may/ be slightly faster. + */ +#undef CAIRO_JPEG_USE_FSTAT + +/*! This is the read block size for the stream reader + * cairo_image_surface_create_from_jpeg_stream(). + */ +#ifdef USE_CAIRO_READ_FUNC_LEN_T +#define CAIRO_JPEG_IO_BLOCK_SIZE 4096 +#else +/*! Block size has to be one if cairo_read_func_t is in use because of the lack + * to detect EOF (truncated reads). + */ +#define CAIRO_JPEG_IO_BLOCK_SIZE 1 +/*! In case of original cairo_read_func_t is used fstat() should be used for + * performance reasons (see CAIRO_JPEG_USE_FSTAT above). + */ +#define CAIRO_JPEG_USE_FSTAT +#endif + +/*! Define this to test jpeg creation with non-image surfaces. This is only for + * testing and is to be used together with CAIRO_JPEG_MAIN. + */ +#undef CAIRO_JPEG_TEST_SIMILAR +#if defined(CAIRO_JPEG_TEST_SIMILAR) && defined(CAIRO_JPEG_MAIN) +#include <cairo-pdf.h> +#endif + +#ifndef LIBJPEG_TURBO_VERSION +/*! This function makes a covnersion for "odd" pixel sizes which typically is a + * conversion from a 3-byte to a 4-byte (or more) pixel size or vice versa. + * The conversion is done from the source buffer src to the destination buffer + * dst. The caller MUST ensure that src and dst have the correct memory size. + * This is dw * num for dst and sw * num for src. src and dst may point to the + * same memory address. + * @param dst Pointer to destination buffer. + * @param dw Pixel width (in bytes) of pixels in destination buffer, dw >= 3. + * @param src Pointer to source buffer. + * @param sw Pixel width (in bytes) of pixels in source buffer, sw >= 3. + * @param num Number of pixels to convert, num >= 1; + */ +static void pix_conv(unsigned char *dst, int dw, const unsigned char *src, int sw, int num) +{ + int si, di; + + // safety check + if (dw < 3 || sw < 3 || dst == NULL || src == NULL) + return; + + num--; + for (si = num * sw, di = num * dw; si >= 0; si -= sw, di -= dw) { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + dst[di + 2] = src[si]; + dst[di + 1] = src[si + 1]; + dst[di + 0] = src[si + 2]; +#else + // FIXME: This is untested, it may be wrong. + dst[di - 3] = src[si - 3]; + dst[di - 2] = src[si - 2]; + dst[di - 1] = src[si - 1]; +#endif + } +} +#endif + +/*! This function creates a JPEG file in memory from a Cairo image surface. + * @param sfc Pointer to a Cairo surface. It should be an image surface of + * either CAIRO_FORMAT_ARGB32 or CAIRO_FORMAT_RGB24. Other formats are + * converted to CAIRO_FORMAT_RGB24 before compression. + * Please note that this may give unexpected results because JPEG does not + * support transparency. Thus, default background color is used to replace + * transparent regions. The default background color is black if not specified + * explicitly. Thus converting e.g. PDF surfaces without having any specific + * background color set will apear with black background and not white as you + * might expect. In such cases it is suggested to manually convert the surface + * to RGB24 before calling this function. + * @param data Pointer to a memory pointer. This parameter receives a pointer + * to the memory area where the final JPEG data is found in memory. This + * function reserves the memory properly and it has to be freed by the caller + * with free(3). + * @param len Pointer to a variable of type size_t which will receive the final + * lenght of the memory buffer. + * @param quality Compression quality, 0-100. + * @return On success the function returns CAIRO_STATUS_SUCCESS. In case of + * error CAIRO_STATUS_INVALID_FORMAT is returned. + */ +cairo_status_t cairo_image_surface_write_to_jpeg_mem(cairo_surface_t *sfc, unsigned char **data, + size_t *len, int quality) +{ + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + JSAMPROW row_pointer[1]; + cairo_surface_t *other = NULL; + + // check valid input format (must be IMAGE_SURFACE && (ARGB32 || RGB24)) + if (cairo_surface_get_type(sfc) != CAIRO_SURFACE_TYPE_IMAGE || + (cairo_image_surface_get_format(sfc) != CAIRO_FORMAT_ARGB32 && + cairo_image_surface_get_format(sfc) != CAIRO_FORMAT_RGB24)) { + // create a similar surface with a proper format if supplied input format + // does not fulfill the requirements + double x1, y1, x2, y2; + other = sfc; + cairo_t *ctx = cairo_create(other); + // get extents of original surface + cairo_clip_extents(ctx, &x1, &y1, &x2, &y2); + cairo_destroy(ctx); + + // create new image surface + sfc = cairo_surface_create_similar_image(other, CAIRO_FORMAT_RGB24, x2 - x1, + y2 - y1); + if (cairo_surface_status(sfc) != CAIRO_STATUS_SUCCESS) + return CAIRO_STATUS_INVALID_FORMAT; + + // paint original surface to new surface + ctx = cairo_create(sfc); + cairo_set_source_surface(ctx, other, 0, 0); + cairo_paint(ctx); + cairo_destroy(ctx); + } + + // finish queued drawing operations + cairo_surface_flush(sfc); + + // init jpeg compression structures + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + + // set compression parameters + jpeg_mem_dest(&cinfo, data, len); + cinfo.image_width = cairo_image_surface_get_width(sfc); + cinfo.image_height = cairo_image_surface_get_height(sfc); +#ifdef LIBJPEG_TURBO_VERSION +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + //cinfo.in_color_space = JCS_EXT_BGRX; + cinfo.in_color_space = cairo_image_surface_get_format(sfc) == CAIRO_FORMAT_ARGB32 ? + JCS_EXT_BGRA : + JCS_EXT_BGRX; +#else + //cinfo.in_color_space = JCS_EXT_XRGB; + cinfo.in_color_space = cairo_image_surface_get_format(sfc) == CAIRO_FORMAT_ARGB32 ? + JCS_EXT_ARGB : + JCS_EXT_XRGB; +#endif + cinfo.input_components = 4; +#else + cinfo.in_color_space = JCS_RGB; + cinfo.input_components = 3; +#endif + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + + // start compressor + jpeg_start_compress(&cinfo, TRUE); + + // loop over all lines and compress + while (cinfo.next_scanline < cinfo.image_height) { +#ifdef LIBJPEG_TURBO_VERSION + row_pointer[0] = cairo_image_surface_get_data(sfc) + + (cinfo.next_scanline * cairo_image_surface_get_stride(sfc)); +#else + unsigned char row_buf[3 * cinfo.image_width]; + pix_conv(row_buf, 3, + cairo_image_surface_get_data(sfc) + + (cinfo.next_scanline * cairo_image_surface_get_stride(sfc)), + 4, cinfo.image_width); + row_pointer[0] = row_buf; +#endif + (void)jpeg_write_scanlines(&cinfo, row_pointer, 1); + } + + // finalize and close everything + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + + // destroy temporary image surface (if available) + if (other != NULL) + cairo_surface_destroy(sfc); + + return CAIRO_STATUS_SUCCESS; +} + +/*! This is the internal write function which is called by + * cairo_image_surface_write_to_jpeg(). It is not exported. + */ +static cairo_status_t cj_write(void *closure, const unsigned char *data, unsigned int length) +{ + return write((intptr_t)closure, data, length) < length ? CAIRO_STATUS_WRITE_ERROR : + CAIRO_STATUS_SUCCESS; +} + +/*! This function writes JPEG file data from a Cairo image surface by using the + * user-supplied stream writer function write_func(). + * @param sfc Pointer to a Cairo *image* surface. Its format must either be + * CAIRO_FORMAT_ARGB32 or CAIRO_FORMAT_RGB24. Other formats are not supported + * by this function, yet. + * @param write_func Function pointer to a function which is actually writing + * the data. + * @param closure Pointer to user-supplied variable which is directly passed to + * write_func(). + * @param quality Compression quality, 0-100. + * @return This function calles cairo_image_surface_write_to_jpeg_mem() and + * returns its return value. + */ +cairo_status_t cairo_image_surface_write_to_jpeg_stream(cairo_surface_t *sfc, + cairo_write_func_t write_func, + void *closure, int quality) +{ + cairo_status_t e; + unsigned char *data = NULL; + size_t len = 0; + + // create JPEG data in memory from surface + if ((e = cairo_image_surface_write_to_jpeg_mem(sfc, &data, &len, quality)) != + CAIRO_STATUS_SUCCESS) + return e; + + // write whole memory block with stream function + e = write_func(closure, data, len); + + // free JPEG memory again and return the return value + free(data); + return e; +} + +/*! This function creates a JPEG file from a Cairo image surface. + * @param sfc Pointer to a Cairo *image* surface. Its format must either be + * CAIRO_FORMAT_ARGB32 or CAIRO_FORMAT_RGB24. Other formats are not supported + * by this function, yet. + * @param filename Pointer to the filename. + * @param quality Compression quality, 0-100. + * @return In case of success CAIRO_STATUS_SUCCESS is returned. If an error + * occured while opening/creating the file CAIRO_STATUS_DEVICE_ERROR is + * returned. The error can be tracked down by inspecting errno(3). The function + * internally calles cairo_image_surface_write_to_jpeg_stream() and returnes + * its return value respectively (see there). + */ +cairo_status_t cairo_image_surface_write_to_jpeg(cairo_surface_t *sfc, const char *filename, + int quality) +{ + cairo_status_t e; + int outfile; + + // Open/create new file + if ((outfile = open(filename, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == + -1) + return CAIRO_STATUS_DEVICE_ERROR; + + // write surface to file + e = cairo_image_surface_write_to_jpeg_stream(sfc, cj_write, (void *)(intptr_t)outfile, + quality); + + // close file again and return + close(outfile); + return e; +} + +/*! This function decompresses a JPEG image from a memory buffer and creates a + * Cairo image surface. + * @param data Pointer to JPEG data (i.e. the full contents of a JPEG file read + * into this buffer). + * @param len Length of buffer in bytes. + * @return Returns a pointer to a cairo_surface_t structure. It should be + * checked with cairo_surface_status() for errors. + */ +cairo_surface_t *cairo_image_surface_create_from_jpeg_mem(void *data, size_t len) +{ + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + JSAMPROW row_pointer[1]; + cairo_surface_t *sfc; + + // initialize jpeg decompression structures + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, data, len); + (void)jpeg_read_header(&cinfo, TRUE); + +#ifdef LIBJPEG_TURBO_VERSION +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + cinfo.out_color_space = JCS_EXT_BGRA; +#else + cinfo.out_color_space = JCS_EXT_ARGB; +#endif +#else + cinfo.out_color_space = JCS_RGB; +#endif + + // start decompressor + (void)jpeg_start_decompress(&cinfo); + + // create Cairo image surface + sfc = cairo_image_surface_create(CAIRO_FORMAT_RGB24, cinfo.output_width, + cinfo.output_height); + if (cairo_surface_status(sfc) != CAIRO_STATUS_SUCCESS) { + jpeg_destroy_decompress(&cinfo); + return sfc; + } + + // loop over all scanlines and fill Cairo image surface + while (cinfo.output_scanline < cinfo.output_height) { + unsigned char *row_address = + cairo_image_surface_get_data(sfc) + + (cinfo.output_scanline * cairo_image_surface_get_stride(sfc)); + row_pointer[0] = row_address; + (void)jpeg_read_scanlines(&cinfo, row_pointer, 1); +#ifndef LIBJPEG_TURBO_VERSION + pix_conv(row_address, 4, row_address, 3, cinfo.output_width); +#endif + } + + // finish and close everything + cairo_surface_mark_dirty(sfc); + (void)jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + + // set jpeg mime data + cairo_surface_set_mime_data(sfc, CAIRO_MIME_TYPE_JPEG, data, len, free, data); + + return sfc; +} + +/*! This function reads a JPEG image from a stream and creates a Cairo image + * surface. + * @param read_func Pointer to function which reads data. + * @param closure Pointer which is passed to read_func(). + * @return Returns a pointer to a cairo_surface_t structure. It should be + * checked with cairo_surface_status() for errors. + * @note If the surface returned is invalid you can use errno(3) to determine + * further reasons. Errno is set according to realloc(3). If you + * intend to check errno you shall set it to 0 before calling this function + * because it modifies errno only in case of an error. + */ +#ifdef USE_CAIRO_READ_FUNC_LEN_T +cairo_surface_t *cairo_image_surface_create_from_jpeg_stream(cairo_read_func_len_t read_func, + void *closure) +#else +cairo_surface_t *cairo_image_surface_create_from_jpeg_stream(cairo_read_func_t read_func, + void *closure) +#endif +{ + cairo_surface_t *sfc; + void *data, *tmp; + ssize_t len, rlen; + int eof = 0; + + // read all data into memory buffer in blocks of CAIRO_JPEG_IO_BLOCK_SIZE + for (len = 0, data = NULL; !eof; len += rlen) { + // grow memory buffer and check for error + if ((tmp = realloc(data, len + CAIRO_JPEG_IO_BLOCK_SIZE)) == NULL) + break; + data = tmp; + + // read bytes into buffer and check for error + rlen = read_func(closure, data + len, CAIRO_JPEG_IO_BLOCK_SIZE); +#ifdef USE_CAIRO_READ_FUNC_LEN_T + // check for error + if (rlen == -1) + break; + + // check if EOF occured + if (rlen < CAIRO_JPEG_IO_BLOCK_SIZE) + eof++; +#else + // check for error + if (rlen == CAIRO_STATUS_READ_ERROR) + eof++; +#endif + } + + // check for error in read loop + if (!eof) { + free(data); + return cairo_image_surface_create(CAIRO_FORMAT_INVALID, 0, 0); + } + + // call jpeg decompression and return surface + sfc = cairo_image_surface_create_from_jpeg_mem(data, len); + if (cairo_surface_status(sfc) != CAIRO_STATUS_SUCCESS) + free(data); + + return sfc; +} + +#ifdef CAIRO_JPEG_USE_FSTAT +/*! This function reads an JPEG image from a file an creates a Cairo image + * surface. Internally the filesize is determined with fstat(2) and then the + * whole data is read at once. + * @param filename Pointer to filename of JPEG file. + * @return Returns a pointer to a cairo_surface_t structure. It should be + * checked with cairo_surface_status() for errors. + * @note If the returned surface is invalid you can use errno to determine + * further reasons. Errno is set according to fopen(3) and malloc(3). If you + * intend to check errno you shall set it to 0 before calling this function + * because it does not modify errno itself. + */ +cairo_surface_t *cairo_image_surface_create_from_jpeg(const char *filename) +{ + void *data; + int infile; + struct stat stat; + + // open input file + if ((infile = open(filename, O_RDONLY)) == -1) + return cairo_image_surface_create(CAIRO_FORMAT_INVALID, 0, 0); + + // get stat structure for file size + if (fstat(infile, &stat) == -1) + return cairo_image_surface_create(CAIRO_FORMAT_INVALID, 0, 0); + + // allocate memory + if ((data = malloc(stat.st_size)) == NULL) + return cairo_image_surface_create(CAIRO_FORMAT_INVALID, 0, 0); + + // read data + if (read(infile, data, stat.st_size) < stat.st_size) + return cairo_image_surface_create(CAIRO_FORMAT_INVALID, 0, 0); + + close(infile); + + return cairo_image_surface_create_from_jpeg_mem(data, stat.st_size); +} + +#else + +/*! This is the read function which is called by + * cairo_image_surface_create_from_jpeg_stream() (non-fstat-version below). It + * is not exported. + */ +#ifdef USE_CAIRO_READ_FUNC_LEN_T +static ssize_t cj_read(void *closure, unsigned char *data, unsigned int length) +{ + return read((intptr_t)closure, data, length); +} +#else +static cairo_status_t cj_read(void *closure, unsigned char *data, unsigned int length) +{ + return read((intptr_t)closure, data, length) < length ? CAIRO_STATUS_READ_ERROR : + CAIRO_STATUS_SUCCESS; +} +#endif + +/*! This function reads an JPEG image from a file an creates a Cairo image + * surface. Internally the function calls + * cairo_image_surface_create_from_jpeg_stream() to actually read the data. + * @param filename Pointer to filename of JPEG file. + * @return Returns a pointer to a cairo_surface_t structure. It should be + * checked with cairo_surface_status() for errors. + * @note If the returned surface is invalid you can use errno to determine + * further reasons. Errno is set according to fopen(3) and malloc(3). If you + * intend to check errno you shall set it to 0 before calling this function + * because it does not modify errno itself. + */ +cairo_surface_t *cairo_image_surface_create_from_jpeg(const char *filename) +{ + cairo_surface_t *sfc; + int infile; + + // open input file + if ((infile = open(filename, O_RDONLY)) == -1) + return cairo_image_surface_create(CAIRO_FORMAT_INVALID, 0, 0); + + // call stream loading function + sfc = cairo_image_surface_create_from_jpeg_stream(cj_read, (void *)(intptr_t)infile); + close(infile); + + return sfc; +} + +#endif + +#ifdef CAIRO_JPEG_MAIN +#include <string.h> +#include <strings.h> + +int strrcasecmp(const char *s1, const char *s2) +{ + int off = (int)strlen(s1) - + (int)strlen(s2); // typecast size_t to int because size_t typically is unsigned + return strcasecmp(s1 + (off < 0 ? 0 : off), s2); +} + +/*! Main routine, only for testing. #undef CAIRO_JPEG_MAIN or simply delete + * this part if you link this file to your own program. + */ +int main(int argc, char **argv) +{ + cairo_surface_t *sfc; + +#ifndef CAIRO_JPEG_TEST_SIMILAR + if (argc < 3) { + fprintf(stderr, "usage: %s <infile> <outfile>\n", argv[0]); + return 1; + } + + // test input file type and read file + if (!strrcasecmp(argv[1], ".png")) { + // read PNG file + sfc = cairo_image_surface_create_from_png(argv[1]); + } else if (!strrcasecmp(argv[1], ".jpg")) { + // read JPEG file + sfc = cairo_image_surface_create_from_jpeg(argv[1]); + } else { + fprintf(stderr, "source file is neither JPG nor PNG\n"); + return 1; + } + + // check surface status + if (cairo_surface_status(sfc) != CAIRO_STATUS_SUCCESS) { + fprintf(stderr, "error loading image: %s", + cairo_status_to_string(cairo_surface_status(sfc))); + return 2; + } + + // test output file type and write file + if (!strrcasecmp(argv[2], ".png")) { + // write PNG file + cairo_surface_write_to_png(sfc, argv[2]); + } else if (!strrcasecmp(argv[2], ".jpg")) { + // write JPEG file + cairo_image_surface_write_to_jpeg(sfc, argv[2], 90); + } else { + fprintf(stderr, "destination file is neither JPG nor PNG\n"); + return 1; + } + + cairo_surface_destroy(sfc); + +#else + sfc = cairo_pdf_surface_create("xyz.pdf", 595.276, 841.890); + + cairo_t *ctx = cairo_create(sfc); + cairo_set_source_rgb(ctx, 1, 1, 1); + cairo_paint(ctx); + cairo_move_to(ctx, 100, 100); + cairo_set_source_rgb(ctx, 1, 0, 0); + cairo_set_line_width(ctx, 3); + cairo_line_to(ctx, 400, 400); + cairo_stroke(ctx); + cairo_destroy(ctx); + + cairo_image_surface_write_to_jpeg(sfc, "xyz.jpg", 90); + cairo_surface_destroy(sfc); +#endif + + return 0; +} + +#endif diff --git a/zathura-note/cairo_jpg.h b/zathura-note/cairo_jpg.h new file mode 100644 index 0000000..5467581 --- /dev/null +++ b/zathura-note/cairo_jpg.h @@ -0,0 +1,75 @@ +/* Copyright 2018 Bernhard R. Fischer, 4096R/8E24F29D <bf@abenteuerland.at> + * + * This file is part of Cairo_JPG. + * + * Cairo_JPG is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Cairo_JPG 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Cairo_JPG. If not, see <https://www.gnu.org/licenses/>. + */ + +#ifndef CAIRO_JPEG_H +#define CAIRO_JPEG_H + +/*! \file cairo_jpg.h + * This file contains all prototypes for the Cairo-JPEG functions implemented + * in cairo_jpg.c. See there for the function documentation. + * + * @author Bernhard R. Fischer, 4096R/8E24F29D <bf@abenteuerland.at> + * @version 2018/12/11 + * @license LGPL3 + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include <cairo.h> +#include <stddef.h> + +#ifdef USE_CAIRO_READ_FUNC_LEN_T +/*! This is the type for the stream read function. Which must be implemented by + * the user if cairo_image_surface_create_from_jpeg_stream() is used. Please + * note that this prototype is slightly different from cairo_read_func_t which + * is used by cairo_image_surface_create_from_png_stream(). + * This new prototype is defined because the original prototype + * cairo_read_func_t does not allow to detect truncated reads. This issue was + * discussed on the cairographics mailinglist, see + * https://lists.cairographics.org/archives/cairo/2016-March/027298.html + * @param closure This parameter is directly passed through by + * cairo_image_surface_create_from_jpeg_stream(). + * @param data Pointer to data buffer which will receive the data. + * @param length Size of the data buffer in bytes. + * @return This function must return the actually length that was read into the + * buffer. This may actually be less than length which indicates an EOF. In + * case of any fatal unrecoverable error on the input stream -1 shall be + * returned. + */ +typedef ssize_t (*cairo_read_func_len_t)(void *closure, unsigned char *data, unsigned int length); +#endif + +cairo_status_t cairo_image_surface_write_to_jpeg_mem(cairo_surface_t *sfc, unsigned char **data, + size_t *len, int quality); +cairo_status_t cairo_image_surface_write_to_jpeg_stream(cairo_surface_t *sfc, + cairo_write_func_t write_func, + void *closure, int quality); +cairo_status_t cairo_image_surface_write_to_jpeg(cairo_surface_t *sfc, const char *filename, + int quality); +cairo_surface_t *cairo_image_surface_create_from_jpeg_mem(void *data, size_t len); +#ifdef USE_CAIRO_READ_FUNC_LEN_T +cairo_surface_t *cairo_image_surface_create_from_jpeg_stream(cairo_read_func_len_t read_func, + void *closure); +#else +cairo_surface_t *cairo_image_surface_create_from_jpeg_stream(cairo_read_func_t read_func, + void *closure); +#endif +cairo_surface_t *cairo_image_surface_create_from_jpeg(const char *filename); + +#endif diff --git a/zathura-note/note.c b/zathura-note/note.c index d25ba0c..282e17b 100644 --- a/zathura-note/note.c +++ b/zathura-note/note.c @@ -1,19 +1,59 @@ +// Copyright (c) 2021 Marvin Borner +// WTFPL License (only for note.*) + +#include "cairo_jpg.h" #include "plugin.h" #include <plist/plist.h> #include <stdio.h> #include <zip.h> +// Data struct for entire document typedef struct { zip_t *zip; plist_t objects; + char *root_name; double width, height; // Page size is constant } note_document_t; +// Data struct for single page +typedef struct { + double start, end; + cairo_t *cairo; + zathura_page_t *page; +} note_page_t; + // Found by reverse engineering #define SESSION_OBJECTS_GENERAL_INFO 1 #define SESSION_OBJECTS_LAYOUT_INFO 2 +static void zip_load(zip_t *zip, const char *root_name, const char *path, void **buf, + size_t *length) +{ + char name[1024] = { 0 }; + snprintf(name, sizeof(name), "%s/%s", root_name, path); + zip_stat_t stat; + zip_stat(zip, name, 0, &stat); + zip_file_t *file = zip_fopen(zip, name, 0); + if (!file) { + zip_error_t *err = zip_get_error(zip); + fprintf(stderr, "Couldn't find '%s' in zip: %s\n", name, zip_error_strerror(err)); + *buf = 0; + *length = 0; + return; + } + + *buf = malloc(stat.size); + *length = zip_fread(file, *buf, stat.size); + if (*length < stat.size) { + fprintf(stderr, "Unexpected size difference\n"); + free(*buf); + *buf = 0; + *length = 0; + return; + } +} + // For debugging/reverse engineering #define INDENT 4 static void plist_dump(plist_t plist, int depth) @@ -112,32 +152,17 @@ static void plist_dump(plist_t plist, int depth) static zathura_error_t plist_load(zip_t *zip, plist_t *plist, const char *root_name, const char *path) { - char name[1024] = { 0 }; - snprintf(name, sizeof(name), "%s/%s", root_name, path); - zip_stat_t stat; - zip_stat(zip, name, 0, &stat); - zip_file_t *file = zip_fopen(zip, name, 0); - if (!file) { - zip_error_t *err = zip_get_error(zip); - fprintf(stderr, "Couldn't find '%s' in zip: %s\n", name, zip_error_strerror(err)); - return ZATHURA_ERROR_INVALID_ARGUMENTS; - } + void *bin; + size_t length; + zip_load(zip, root_name, path, &bin, &length); - void *bin = malloc(stat.size); - size_t length = zip_fread(file, bin, stat.size); - if (length < stat.size) { - fprintf(stderr, "Unexpected size difference\n"); - free(bin); - return ZATHURA_ERROR_INVALID_ARGUMENTS; - } - - if (!plist_is_binary(bin, stat.size)) { + if (!plist_is_binary(bin, length)) { fprintf(stderr, "Unexpected file format of '%s'\n", path); free(bin); return ZATHURA_ERROR_INVALID_ARGUMENTS; } - plist_from_bin(bin, stat.size, plist); + plist_from_bin(bin, length, plist); free(bin); return ZATHURA_ERROR_OK; } @@ -162,17 +187,23 @@ static plist_t plist_access(plist_t plist, int length, ...) va_start(va, length); unsigned long uid = 0; - const char **ptr; + const char **ptr, *dict_key; plist_t current = plist; - int i; + int i, array_index; for (i = 0; i < length && current; i++) { plist_type type = plist_get_node_type(current); switch (type) { case PLIST_ARRAY: - current = plist_array_get_item(current, va_arg(va, int)); + array_index = va_arg(va, int); + current = plist_array_get_item(current, array_index); + if (!current) + fprintf(stderr, "Couldn't find %d in array\n", array_index); break; case PLIST_DICT: - current = plist_dict_get_item(current, va_arg(va, const char *)); + dict_key = va_arg(va, const char *); + current = plist_dict_get_item(current, dict_key); + if (!current) + fprintf(stderr, "Couldn't find '%s' in dict\n", dict_key); break; case PLIST_UID: // Automatic tracing! plist_get_uid_val(current, &uid); @@ -207,7 +238,7 @@ static plist_t plist_access(plist_t plist, int length, ...) plist_get_real_val(current, va_arg(va, double *)); goto end; default: - fprintf(stderr, "Fatal failure in access loop\n"); + fprintf(stderr, "Unknown plist type in access loop\n"); } } @@ -225,7 +256,7 @@ end: // TODO: Exit entire zathura in these conditions? Hmmm if (!current) - fprintf(stderr, "Even more fatal failure in access loop\n"); + fprintf(stderr, "Fatal failure in access loop\n"); return current; } @@ -243,6 +274,14 @@ static plist_t plist_handwriting_overlay(plist_t objects) return overlay; } +// Converts the strange "{42.123, 69.123}" format to respective floats +static void plist_string_to_floats(const char *string, float *a, float *b) +{ + char *end; + *a = strtof(string + 1, &end); + *b = strtof(end + 2, NULL); +} + // TODO: Find more elegant solution for page count (there doesn't seem to be) static int plist_page_count(plist_t objects, double page_height) { @@ -335,11 +374,13 @@ GIRARA_HIDDEN zathura_error_t note_document_open(zathura_document_t *document) note_document->objects = plist_dict_get_item(session_plist, "$objects"); if (!PLIST_IS_ARRAY(note_document->objects)) { fprintf(stderr, "Invalid objects type\n"); + free(note_document); free(root_name); return ZATHURA_ERROR_NOT_IMPLEMENTED; } note_document->zip = zip; + note_document->root_name = root_name; note_document->width = plist_page_width(note_document->objects); if (note_document->width < 1) { @@ -352,17 +393,19 @@ GIRARA_HIDDEN zathura_error_t note_document_open(zathura_document_t *document) zathura_document_set_number_of_pages(document, plist_page_count(note_document->objects, note_document->height)); - free(root_name); return ZATHURA_ERROR_OK; } GIRARA_HIDDEN zathura_error_t note_document_free(zathura_document_t *document, void *data) { + (void)document; + if (!data) return ZATHURA_ERROR_OK; note_document_t *note_document = data; zip_close(note_document->zip); + free(note_document->root_name); return ZATHURA_ERROR_OK; } @@ -372,24 +415,143 @@ GIRARA_HIDDEN zathura_error_t note_page_init(zathura_page_t *page) zathura_page_set_width(page, note_document->width); zathura_page_set_height(page, note_document->height); + double height = zathura_page_get_height(page); + unsigned int number = zathura_page_get_index(page); + + note_page_t *note_page = malloc(sizeof(*note_page)); + note_page->page = page; + note_page->start = height * number; + note_page->end = height * (number + 1); + zathura_page_set_data(page, note_page); + return ZATHURA_ERROR_OK; } GIRARA_HIDDEN zathura_error_t note_page_clear(zathura_page_t *page, void *data) { + (void)page; + free(data); return ZATHURA_ERROR_OK; } -static void note_page_render_object(cairo_t *cairo, plist_t objects, int index) +typedef struct { + char *data; + size_t length; +} cairo_read_closure; + +// Cairo is weird. Why can't we just pass a data buffer directly (like with cairo_jpg)?! +static cairo_status_t cairo_read(void *data, unsigned char *buf, unsigned int length) { + cairo_read_closure *closure = data; + + if (length > closure->length) + return CAIRO_STATUS_READ_ERROR; + + memcpy(buf, closure->data, length); + + closure->length -= length; + closure->data += length; + + return CAIRO_STATUS_SUCCESS; +} + +static cairo_surface_t *cairo_surface_scale(cairo_surface_t *surface, float width, float height) +{ + int unscaled_width = cairo_image_surface_get_width(surface); + int unscaled_height = cairo_image_surface_get_height(surface); + cairo_surface_t *result = cairo_surface_create_similar( + surface, cairo_surface_get_content(surface), width, height); + cairo_t *cairo = cairo_create(result); + cairo_scale(cairo, width / (float)unscaled_width, height / (float)unscaled_height); + cairo_set_source_surface(cairo, surface, 0, 0); + cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); + cairo_paint(cairo); + cairo_destroy(cairo); + return result; +} + +static void note_page_render_image_object(note_page_t *page, plist_t objects, int index) +{ + char missing = 0; + plist_access(objects, 6, index, "figure", "FigureBackgroundObjectKey", + "kImageObjectSnapshotKey", "imageIsMissing", &missing); + if (missing) + return; + char *position = 0; size_t position_length = 0; plist_access(objects, 4, index, "documentContentOrigin", &position, &position_length); - printf("%.*s\n", (int)position_length, position); + float x, y; + plist_string_to_floats(position, &x, &y); + + char *size = 0; + size_t size_length = 0; + plist_access(objects, 4, index, "unscaledContentSize", &size, &size_length); + float width, height; + plist_string_to_floats(size, &width, &height); + + if (y < page->start || y + height > page->end) + return; + + char *path = 0; + size_t path_length = 0; + plist_access(objects, 7, index, "figure", "FigureBackgroundObjectKey", + "kImageObjectSnapshotKey", "relativePath", &path, &path_length); + + char is_jpeg = 0; // 0 means png + plist_access(objects, 6, index, "figure", "FigureBackgroundObjectKey", + "kImageObjectSnapshotKey", "saveAsJPEG", &is_jpeg); + + note_document_t *note_document = + zathura_document_get_data(zathura_page_get_document(page->page)); + zip_t *zip = note_document->zip; + void *data; + size_t length; + zip_load(zip, note_document->root_name, path, &data, &length); + if (!data || !length) { + fprintf(stderr, "Invalid media object '%s' in zip\n", path); + return; + } + + cairo_surface_t *surface = 0; + if (is_jpeg) { + surface = cairo_image_surface_create_from_jpeg_mem(data, length); + } else { + cairo_read_closure closure = { .data = data, .length = length }; + surface = cairo_image_surface_create_from_png_stream(cairo_read, &closure); + } + + if (!surface || cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) { + fprintf(stderr, "Invalid surface from png stream\n"); + return; + } + + surface = cairo_surface_scale(surface, width, height); + + cairo_set_source_surface(page->cairo, surface, x, y - page->start); + cairo_paint(page->cairo); + cairo_surface_flush(surface); + cairo_surface_destroy(surface); +} + +static void note_page_render_object(note_page_t *page, plist_t objects, int index) +{ + char *class = 0; + size_t class_length = 0; + plist_access(objects, 5, index, "$class", "$classname", &class, &class_length); + + if (!memcmp(class, "ImageMediaObject", class_length)) { + note_page_render_image_object(page, objects, index); + } else if (!memcmp(class, "TextBlockMediaObject", class_length)) { + // TODO + } else { + fprintf(stderr, "Unknown media object type '%.*s', please report\n", + (int)class_length, class); + } } // It doesn't really matter if something in here fails -static void note_page_render_objects(cairo_t *cairo, plist_t objects) +static void note_page_render_objects(note_page_t *page, plist_t objects) { plist_t objects_array = plist_access(objects, 3, SESSION_OBJECTS_LAYOUT_INFO, "mediaObjects", "NS.objects"); @@ -404,7 +566,7 @@ static void note_page_render_objects(cairo_t *cairo, plist_t objects) size_t index; plist_get_uid_val(val, &index); - note_page_render_object(cairo, objects, index); + note_page_render_object(page, objects, index); } } @@ -415,6 +577,9 @@ GIRARA_HIDDEN zathura_error_t note_page_render_cairo(zathura_page_t *page, void return ZATHURA_ERROR_NOT_IMPLEMENTED; note_document_t *note_document = zathura_document_get_data(zathura_page_get_document(page)); + note_page_t *note_page = data; + note_page->cairo = cairo; + plist_t overlay = plist_handwriting_overlay(note_document->objects); if (!overlay) return ZATHURA_ERROR_NOT_IMPLEMENTED; @@ -422,13 +587,8 @@ GIRARA_HIDDEN zathura_error_t note_page_render_cairo(zathura_page_t *page, void /* plist_dump(note_document->session_plist, 0); */ /* return ZATHURA_ERROR_OK; */ - double height = zathura_page_get_height(page); - unsigned int number = zathura_page_get_index(page); - double page_start = height * number; - double page_end = height * (number + 1); - // Render all media objects (images, ...) - note_page_render_objects(cairo, note_document->objects); + note_page_render_objects(note_page, note_document->objects); // Array of points on curve size_t curves_length = 0; @@ -463,15 +623,17 @@ GIRARA_HIDDEN zathura_error_t note_page_render_cairo(zathura_page_t *page, void (float)(color[1] & 0xff) / 255, (float)(color[2] & 0xff) / 255, (float)(color[3] & 0xff) / 255); + + // TODO: Fractional curve widths (?) cairo_set_line_width(cairo, curves_width[i]); - if (curves[pos + 1] >= page_start && curves[pos + 1] <= page_end) - cairo_move_to(cairo, curves[pos], curves[pos + 1] - page_start); + if (curves[pos + 1] >= note_page->start && curves[pos + 1] <= note_page->end) + cairo_move_to(cairo, curves[pos], curves[pos + 1] - note_page->start); // TODO: Render as bezier curves for (unsigned int j = pos; j < pos + length * 2; j += 2) - if (curves[j + 1] >= page_start && curves[j + 1] <= page_end) - cairo_line_to(cairo, curves[j], curves[j + 1] - page_start); + if (curves[j + 1] >= note_page->start && curves[j + 1] <= note_page->end) + cairo_line_to(cairo, curves[j], curves[j + 1] - note_page->start); cairo_stroke(cairo); pos += length * 2; |