| // ========================================================== |
| // JPEG lossless transformations |
| // |
| // Design and implementation by |
| // - Petr Pytelka (pyta@lightcomp.com) |
| // - Hervé Drolon (drolon@infonie.fr) |
| // - Mihail Naydenov (mnaydenov@users.sourceforge.net) |
| // |
| // This file is part of FreeImage 3 |
| // |
| // COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY |
| // OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES |
| // THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE |
| // OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED |
| // CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT |
| // THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY |
| // SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL |
| // PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER |
| // THIS DISCLAIMER. |
| // |
| // Use at your own risk! |
| // ========================================================== |
| |
| extern "C" { |
| #define XMD_H |
| #undef FAR |
| #include <setjmp.h> |
| |
| #include "third_party/jpeg/jinclude.h" |
| #include "third_party/jpeg/jpeglib.h" |
| #include "third_party/jpeg/jerror.h" |
| #include "third_party/jpeg/transupp.h" |
| } |
| |
| #include "FreeImage.h" |
| #include "Utilities.h" |
| #include "FreeImageIO.h" |
| |
| // ---------------------------------------------------------- |
| // Source manager & Destination manager setup |
| // (see PluginJPEG.cpp) |
| // ---------------------------------------------------------- |
| |
| void jpeg_freeimage_src(j_decompress_ptr cinfo, fi_handle infile, FreeImageIO *io); |
| void jpeg_freeimage_dst(j_compress_ptr cinfo, fi_handle outfile, FreeImageIO *io); |
| |
| // ---------------------------------------------------------- |
| // Error handling |
| // (see also PluginJPEG.cpp) |
| // ---------------------------------------------------------- |
| |
| /** |
| Receives control for a fatal error. Information sufficient to |
| generate the error message has been stored in cinfo->err; call |
| output_message to display it. Control must NOT return to the caller; |
| generally this routine will exit() or longjmp() somewhere. |
| */ |
| METHODDEF(void) |
| ls_jpeg_error_exit (j_common_ptr cinfo) { |
| // always display the message |
| (*cinfo->err->output_message)(cinfo); |
| |
| // allow JPEG with a premature end of file |
| if((cinfo)->err->msg_parm.i[0] != 13) { |
| |
| // let the memory manager delete any temp files before we die |
| jpeg_destroy(cinfo); |
| |
| throw FIF_JPEG; |
| } |
| } |
| |
| /** |
| Actual output of any JPEG message. Note that this method does not know |
| how to generate a message, only where to send it. |
| */ |
| METHODDEF(void) |
| ls_jpeg_output_message (j_common_ptr cinfo) { |
| char buffer[JMSG_LENGTH_MAX]; |
| |
| // create the message |
| (*cinfo->err->format_message)(cinfo, buffer); |
| // send it to user's message proc |
| FreeImage_OutputMessageProc(FIF_JPEG, buffer); |
| } |
| |
| // ---------------------------------------------------------- |
| // Main program |
| // ---------------------------------------------------------- |
| |
| /** |
| Build a crop string. |
| |
| @param crop Output crop string |
| @param left Specifies the left position of the cropped rectangle |
| @param top Specifies the top position of the cropped rectangle |
| @param right Specifies the right position of the cropped rectangle |
| @param bottom Specifies the bottom position of the cropped rectangle |
| @param width Image width |
| @param height Image height |
| @return Returns TRUE if successful, returns FALSE otherwise |
| */ |
| static BOOL |
| getCropString(char* crop, int* left, int* top, int* right, int* bottom, int width, int height) { |
| if(!left || !top || !right || !bottom) { |
| return FALSE; |
| } |
| |
| *left = CLAMP(*left, 0, width); |
| *top = CLAMP(*top, 0, height); |
| |
| // negative/zero right and bottom count from the edges inwards |
| |
| if(*right <= 0) { |
| *right = width + *right; |
| } |
| if(*bottom <= 0) { |
| *bottom = height + *bottom; |
| } |
| |
| *right = CLAMP(*right, 0, width); |
| *bottom = CLAMP(*bottom, 0, height); |
| |
| // test for empty rect |
| |
| if(((*left - *right) == 0) || ((*top - *bottom) == 0)) { |
| return FALSE; |
| } |
| |
| // normalize the rectangle |
| |
| if(*right < *left) { |
| INPLACESWAP(*left, *right); |
| } |
| if(*bottom < *top) { |
| INPLACESWAP(*top, *bottom); |
| } |
| |
| // test for "noop" rect |
| |
| if(*left == 0 && *right == width && *top == 0 && *bottom == height) { |
| return FALSE; |
| } |
| |
| // build the crop option |
| sprintf(crop, "%dx%d+%d+%d", *right - *left, *bottom - *top, *left, *top); |
| |
| return TRUE; |
| } |
| |
| static BOOL |
| JPEGTransformFromHandle(FreeImageIO* src_io, fi_handle src_handle, FreeImageIO* dst_io, fi_handle dst_handle, FREE_IMAGE_JPEG_OPERATION operation, int* left, int* top, int* right, int* bottom, BOOL perfect) { |
| const BOOL onlyReturnCropRect = (dst_io == NULL) || (dst_handle == NULL); |
| const long stream_start = onlyReturnCropRect ? 0 : dst_io->tell_proc(dst_handle); |
| BOOL swappedDim = FALSE; |
| BOOL trimH = FALSE; |
| BOOL trimV = FALSE; |
| |
| // Set up the jpeglib structures |
| jpeg_decompress_struct srcinfo; |
| jpeg_compress_struct dstinfo; |
| jpeg_error_mgr jsrcerr, jdsterr; |
| jvirt_barray_ptr *src_coef_arrays = NULL; |
| jvirt_barray_ptr *dst_coef_arrays = NULL; |
| // Support for copying optional markers from source to destination file |
| JCOPY_OPTION copyoption; |
| // Image transformation options |
| jpeg_transform_info transfoptions; |
| |
| // Initialize structures |
| memset(&srcinfo, 0, sizeof(srcinfo)); |
| memset(&jsrcerr, 0, sizeof(jsrcerr)); |
| memset(&jdsterr, 0, sizeof(jdsterr)); |
| memset(&dstinfo, 0, sizeof(dstinfo)); |
| memset(&transfoptions, 0, sizeof(transfoptions)); |
| |
| // Copy all extra markers from source file |
| copyoption = JCOPYOPT_ALL; |
| |
| // Set up default JPEG parameters |
| transfoptions.force_grayscale = FALSE; |
| transfoptions.crop = FALSE; |
| |
| // Select the transform option |
| switch(operation) { |
| case FIJPEG_OP_FLIP_H: // horizontal flip |
| transfoptions.transform = JXFORM_FLIP_H; |
| trimH = TRUE; |
| break; |
| case FIJPEG_OP_FLIP_V: // vertical flip |
| transfoptions.transform = JXFORM_FLIP_V; |
| trimV = TRUE; |
| break; |
| case FIJPEG_OP_TRANSPOSE: // transpose across UL-to-LR axis |
| transfoptions.transform = JXFORM_TRANSPOSE; |
| swappedDim = TRUE; |
| break; |
| case FIJPEG_OP_TRANSVERSE: // transpose across UR-to-LL axis |
| transfoptions.transform = JXFORM_TRANSVERSE; |
| trimH = TRUE; |
| trimV = TRUE; |
| swappedDim = TRUE; |
| break; |
| case FIJPEG_OP_ROTATE_90: // 90-degree clockwise rotation |
| transfoptions.transform = JXFORM_ROT_90; |
| trimH = TRUE; |
| swappedDim = TRUE; |
| break; |
| case FIJPEG_OP_ROTATE_180: // 180-degree rotation |
| trimH = TRUE; |
| trimV = TRUE; |
| transfoptions.transform = JXFORM_ROT_180; |
| break; |
| case FIJPEG_OP_ROTATE_270: // 270-degree clockwise (or 90 ccw) |
| transfoptions.transform = JXFORM_ROT_270; |
| trimV = TRUE; |
| swappedDim = TRUE; |
| break; |
| default: |
| case FIJPEG_OP_NONE: // no transformation |
| transfoptions.transform = JXFORM_NONE; |
| break; |
| } |
| // (perfect == TRUE) ==> fail if there is non-transformable edge blocks |
| transfoptions.perfect = (perfect == TRUE) ? TRUE : FALSE; |
| // Drop non-transformable edge blocks: trim off any partial edge MCUs that the transform can't handle. |
| transfoptions.trim = TRUE; |
| |
| try { |
| |
| // Initialize the JPEG decompression object with default error handling |
| srcinfo.err = jpeg_std_error(&jsrcerr); |
| srcinfo.err->error_exit = ls_jpeg_error_exit; |
| srcinfo.err->output_message = ls_jpeg_output_message; |
| jpeg_create_decompress(&srcinfo); |
| |
| // Initialize the JPEG compression object with default error handling |
| dstinfo.err = jpeg_std_error(&jdsterr); |
| dstinfo.err->error_exit = ls_jpeg_error_exit; |
| dstinfo.err->output_message = ls_jpeg_output_message; |
| jpeg_create_compress(&dstinfo); |
| |
| // Specify data source for decompression |
| jpeg_freeimage_src(&srcinfo, src_handle, src_io); |
| |
| // Enable saving of extra markers that we want to copy |
| jcopy_markers_setup(&srcinfo, copyoption); |
| |
| // Read the file header |
| jpeg_read_header(&srcinfo, TRUE); |
| |
| // crop option |
| char crop[64]; |
| const BOOL hasCrop = getCropString(crop, left, top, right, bottom, swappedDim ? srcinfo.image_height : srcinfo.image_width, swappedDim ? srcinfo.image_width : srcinfo.image_height); |
| |
| if(hasCrop) { |
| if(!jtransform_parse_crop_spec(&transfoptions, crop)) { |
| FreeImage_OutputMessageProc(FIF_JPEG, "Bogus crop argument %s", crop); |
| throw(1); |
| } |
| } |
| |
| // Any space needed by a transform option must be requested before |
| // jpeg_read_coefficients so that memory allocation will be done right |
| |
| // Prepare transformation workspace |
| // Fails right away if perfect flag is TRUE and transformation is not perfect |
| if( !jtransform_request_workspace(&srcinfo, &transfoptions) ) { |
| FreeImage_OutputMessageProc(FIF_JPEG, "Transformation is not perfect"); |
| throw(1); |
| } |
| |
| if(left || top) { |
| // compute left and top offsets, it's a bit tricky, taking into account both |
| // transform, which might have trimed the image, |
| // and crop itself, which is adjusted to lie on a iMCU boundary |
| |
| const int fullWidth = swappedDim ? srcinfo.image_height : srcinfo.image_width; |
| const int fullHeight = swappedDim ? srcinfo.image_width : srcinfo.image_height; |
| |
| int transformedFullWidth = fullWidth; |
| int transformedFullHeight = fullHeight; |
| |
| if(trimH && transformedFullWidth/transfoptions.iMCU_sample_width > 0) { |
| transformedFullWidth = (transformedFullWidth/transfoptions.iMCU_sample_width) * transfoptions.iMCU_sample_width; |
| } |
| if(trimV && transformedFullHeight/transfoptions.iMCU_sample_height > 0) { |
| transformedFullHeight = (transformedFullHeight/transfoptions.iMCU_sample_height) * transfoptions.iMCU_sample_height; |
| } |
| |
| const int trimmedWidth = fullWidth - transformedFullWidth; |
| const int trimmedHeight = fullHeight - transformedFullHeight; |
| |
| if(left) { |
| *left = trimmedWidth + transfoptions.x_crop_offset * transfoptions.iMCU_sample_width; |
| } |
| if(top) { |
| *top = trimmedHeight + transfoptions.y_crop_offset * transfoptions.iMCU_sample_height; |
| } |
| } |
| |
| if(right) { |
| *right = (left ? *left : 0) + transfoptions.output_width; |
| } |
| if(bottom) { |
| *bottom = (top ? *top : 0) + transfoptions.output_height; |
| } |
| |
| // if only the crop rect is requested, we are done |
| |
| if(onlyReturnCropRect) { |
| jpeg_destroy_compress(&dstinfo); |
| jpeg_destroy_decompress(&srcinfo); |
| return TRUE; |
| } |
| |
| // Read source file as DCT coefficients |
| src_coef_arrays = jpeg_read_coefficients(&srcinfo); |
| |
| // Initialize destination compression parameters from source values |
| jpeg_copy_critical_parameters(&srcinfo, &dstinfo); |
| |
| // Adjust destination parameters if required by transform options; |
| // also find out which set of coefficient arrays will hold the output |
| dst_coef_arrays = jtransform_adjust_parameters(&srcinfo, &dstinfo, src_coef_arrays, &transfoptions); |
| |
| // Note: we assume that jpeg_read_coefficients consumed all input |
| // until JPEG_REACHED_EOI, and that jpeg_finish_decompress will |
| // only consume more while (! cinfo->inputctl->eoi_reached). |
| // We cannot call jpeg_finish_decompress here since we still need the |
| // virtual arrays allocated from the source object for processing. |
| |
| if(src_handle == dst_handle) { |
| dst_io->seek_proc(dst_handle, stream_start, SEEK_SET); |
| } |
| |
| // Specify data destination for compression |
| jpeg_freeimage_dst(&dstinfo, dst_handle, dst_io); |
| |
| // Start compressor (note no image data is actually written here) |
| jpeg_write_coefficients(&dstinfo, dst_coef_arrays); |
| |
| // Copy to the output file any extra markers that we want to preserve |
| jcopy_markers_execute(&srcinfo, &dstinfo, copyoption); |
| |
| // Execute image transformation, if any |
| jtransform_execute_transformation(&srcinfo, &dstinfo, src_coef_arrays, &transfoptions); |
| |
| // Finish compression and release memory |
| jpeg_finish_compress(&dstinfo); |
| jpeg_destroy_compress(&dstinfo); |
| jpeg_finish_decompress(&srcinfo); |
| jpeg_destroy_decompress(&srcinfo); |
| |
| } |
| catch(...) { |
| jpeg_destroy_compress(&dstinfo); |
| jpeg_destroy_decompress(&srcinfo); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| // ---------------------------------------------------------- |
| // FreeImage interface |
| // ---------------------------------------------------------- |
| |
| BOOL DLL_CALLCONV |
| FreeImage_JPEGTransformFromHandle(FreeImageIO* src_io, fi_handle src_handle, FreeImageIO* dst_io, fi_handle dst_handle, FREE_IMAGE_JPEG_OPERATION operation, int* left, int* top, int* right, int* bottom, BOOL perfect) { |
| return JPEGTransformFromHandle(src_io, src_handle, dst_io, dst_handle, operation, left, top, right, bottom, perfect); |
| } |
| |
| static void |
| closeStdIO(fi_handle src_handle, fi_handle dst_handle) { |
| if(src_handle) { |
| fclose((FILE*)src_handle); |
| } |
| if(dst_handle && (dst_handle != src_handle)) { |
| fclose((FILE*)dst_handle); |
| } |
| } |
| |
| static BOOL |
| openStdIO(const char* src_file, const char* dst_file, FreeImageIO* dst_io, fi_handle* src_handle, fi_handle* dst_handle) { |
| *src_handle = NULL; |
| *dst_handle = NULL; |
| |
| FreeImageIO io; |
| SetDefaultIO (&io); |
| |
| const BOOL isSameFile = (dst_file && (strcmp(src_file, dst_file) == 0)) ? TRUE : FALSE; |
| |
| FILE* srcp = NULL; |
| FILE* dstp = NULL; |
| |
| if(isSameFile) { |
| srcp = fopen(src_file, "r+b"); |
| dstp = srcp; |
| } |
| else { |
| srcp = fopen(src_file, "rb"); |
| if(dst_file) { |
| dstp = fopen(dst_file, "wb"); |
| } |
| } |
| |
| if(!srcp || (dst_file && !dstp)) { |
| if(!srcp) { |
| FreeImage_OutputMessageProc(FIF_JPEG, "Cannot open \"%s\" for reading", src_file); |
| } else { |
| FreeImage_OutputMessageProc(FIF_JPEG, "Cannot open \"%s\" for writing", dst_file); |
| } |
| closeStdIO(srcp, dstp); |
| return FALSE; |
| } |
| |
| if(FreeImage_GetFileTypeFromHandle(&io, srcp) != FIF_JPEG) { |
| FreeImage_OutputMessageProc(FIF_JPEG, " Source file \"%s\" is not jpeg", src_file); |
| closeStdIO(srcp, dstp); |
| return FALSE; |
| } |
| |
| *dst_io = io; |
| *src_handle = srcp; |
| *dst_handle = dstp; |
| |
| return TRUE; |
| } |
| |
| static BOOL |
| openStdIOU(const wchar_t* src_file, const wchar_t* dst_file, FreeImageIO* dst_io, fi_handle* src_handle, fi_handle* dst_handle) { |
| #ifdef _WIN32 |
| |
| *src_handle = NULL; |
| *dst_handle = NULL; |
| |
| FreeImageIO io; |
| SetDefaultIO (&io); |
| |
| const BOOL isSameFile = (dst_file && (wcscmp(src_file, dst_file) == 0)) ? TRUE : FALSE; |
| |
| FILE* srcp = NULL; |
| FILE* dstp = NULL; |
| |
| if(isSameFile) { |
| srcp = _wfopen(src_file, L"r+b"); |
| dstp = srcp; |
| } else { |
| srcp = _wfopen(src_file, L"rb"); |
| if(dst_file) { |
| dstp = _wfopen(dst_file, L"wb"); |
| } |
| } |
| |
| if(!srcp || (dst_file && !dstp)) { |
| if(!srcp) { |
| FreeImage_OutputMessageProc(FIF_JPEG, "Cannot open source file for reading"); |
| } else { |
| FreeImage_OutputMessageProc(FIF_JPEG, "Cannot open destination file for writing"); |
| } |
| closeStdIO(srcp, dstp); |
| return FALSE; |
| } |
| |
| if(FreeImage_GetFileTypeFromHandle(&io, srcp) != FIF_JPEG) { |
| FreeImage_OutputMessageProc(FIF_JPEG, " Source file is not jpeg"); |
| closeStdIO(srcp, dstp); |
| return FALSE; |
| } |
| |
| *dst_io = io; |
| *src_handle = srcp; |
| *dst_handle = dstp; |
| |
| return TRUE; |
| |
| #else |
| return FALSE; |
| #endif // _WIN32 |
| } |
| |
| BOOL DLL_CALLCONV |
| FreeImage_JPEGTransform(const char *src_file, const char *dst_file, FREE_IMAGE_JPEG_OPERATION operation, BOOL perfect) { |
| FreeImageIO io; |
| fi_handle src; |
| fi_handle dst; |
| |
| if(!openStdIO(src_file, dst_file, &io, &src, &dst)) { |
| return FALSE; |
| } |
| |
| BOOL ret = JPEGTransformFromHandle(&io, src, &io, dst, operation, NULL, NULL, NULL, NULL, perfect); |
| |
| closeStdIO(src, dst); |
| |
| return ret; |
| } |
| |
| BOOL DLL_CALLCONV |
| FreeImage_JPEGCrop(const char *src_file, const char *dst_file, int left, int top, int right, int bottom) { |
| FreeImageIO io; |
| fi_handle src; |
| fi_handle dst; |
| |
| if(!openStdIO(src_file, dst_file, &io, &src, &dst)) { |
| return FALSE; |
| } |
| |
| BOOL ret = FreeImage_JPEGTransformFromHandle(&io, src, &io, dst, FIJPEG_OP_NONE, &left, &top, &right, &bottom, FALSE); |
| |
| closeStdIO(src, dst); |
| |
| return ret; |
| } |
| |
| BOOL DLL_CALLCONV |
| FreeImage_JPEGTransformU(const wchar_t *src_file, const wchar_t *dst_file, FREE_IMAGE_JPEG_OPERATION operation, BOOL perfect) { |
| FreeImageIO io; |
| fi_handle src; |
| fi_handle dst; |
| |
| if(!openStdIOU(src_file, dst_file, &io, &src, &dst)) { |
| return FALSE; |
| } |
| |
| BOOL ret = JPEGTransformFromHandle(&io, src, &io, dst, operation, NULL, NULL, NULL, NULL, perfect); |
| |
| closeStdIO(src, dst); |
| |
| return ret; |
| } |
| |
| BOOL DLL_CALLCONV |
| FreeImage_JPEGCropU(const wchar_t *src_file, const wchar_t *dst_file, int left, int top, int right, int bottom) { |
| FreeImageIO io; |
| fi_handle src; |
| fi_handle dst; |
| |
| if(!openStdIOU(src_file, dst_file, &io, &src, &dst)) { |
| return FALSE; |
| } |
| |
| BOOL ret = FreeImage_JPEGTransformFromHandle(&io, src, &io, dst, FIJPEG_OP_NONE, &left, &top, &right, &bottom, FALSE); |
| |
| closeStdIO(src, dst); |
| |
| return ret; |
| } |
| |
| BOOL DLL_CALLCONV |
| FreeImage_JPEGTransformCombined(const char *src_file, const char *dst_file, FREE_IMAGE_JPEG_OPERATION operation, int* left, int* top, int* right, int* bottom, BOOL perfect) { |
| FreeImageIO io; |
| fi_handle src; |
| fi_handle dst; |
| |
| if(!openStdIO(src_file, dst_file, &io, &src, &dst)) { |
| return FALSE; |
| } |
| |
| BOOL ret = FreeImage_JPEGTransformFromHandle(&io, src, &io, dst, operation, left, top, right, bottom, perfect); |
| |
| closeStdIO(src, dst); |
| |
| return ret; |
| } |
| |
| BOOL DLL_CALLCONV |
| FreeImage_JPEGTransformCombinedU(const wchar_t *src_file, const wchar_t *dst_file, FREE_IMAGE_JPEG_OPERATION operation, int* left, int* top, int* right, int* bottom, BOOL perfect) { |
| FreeImageIO io; |
| fi_handle src; |
| fi_handle dst; |
| |
| if(!openStdIOU(src_file, dst_file, &io, &src, &dst)) { |
| return FALSE; |
| } |
| |
| BOOL ret = FreeImage_JPEGTransformFromHandle(&io, src, &io, dst, operation, left, top, right, bottom, perfect); |
| |
| closeStdIO(src, dst); |
| |
| return ret; |
| } |
| |
| // -------------------------------------------------------------------------- |
| |
| static BOOL |
| getMemIO(FIMEMORY* src_stream, FIMEMORY* dst_stream, FreeImageIO* dst_io, fi_handle* src_handle, fi_handle* dst_handle) { |
| *src_handle = NULL; |
| *dst_handle = NULL; |
| |
| FreeImageIO io; |
| SetMemoryIO (&io); |
| |
| if(dst_stream) { |
| FIMEMORYHEADER *mem_header = (FIMEMORYHEADER*)(dst_stream->data); |
| if(mem_header->delete_me != TRUE) { |
| // do not save in a user buffer |
| FreeImage_OutputMessageProc(FIF_JPEG, "Destination memory buffer is read only"); |
| return FALSE; |
| } |
| } |
| |
| *dst_io = io; |
| *src_handle = src_stream; |
| *dst_handle = dst_stream; |
| |
| return TRUE; |
| } |
| |
| BOOL DLL_CALLCONV |
| FreeImage_JPEGTransformCombinedFromMemory(FIMEMORY* src_stream, FIMEMORY* dst_stream, FREE_IMAGE_JPEG_OPERATION operation, int* left, int* top, int* right, int* bottom, BOOL perfect) { |
| FreeImageIO io; |
| fi_handle src; |
| fi_handle dst; |
| |
| if(!getMemIO(src_stream, dst_stream, &io, &src, &dst)) { |
| return FALSE; |
| } |
| |
| return FreeImage_JPEGTransformFromHandle(&io, src, &io, dst, operation, left, top, right, bottom, perfect); |
| } |
| |