Skip to content

Add code for lossless JPEG transformation #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions jpeg/compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ func finishCompress(cinfo *C.struct_jpeg_compress_struct) error {
return nil
}

func writeCoefficients(cinfo *C.struct_jpeg_compress_struct, coefArrays *C.jvirt_barray_ptr) error {
C.jpeg_write_coefficients(cinfo, coefArrays)
return nil
}

func writeScanline(cinfo *C.struct_jpeg_compress_struct, row C.JSAMPROW, maxLines C.JDIMENSION) (line int, err error) {
code := C.int(0)
line = int(C.write_scanlines(cinfo, row, maxLines, &code))
Expand Down
8 changes: 8 additions & 0 deletions jpeg/decompress.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ func readHeader(dinfo *C.struct_jpeg_decompress_struct) error {
return nil
}

func readCoefficients(dinfo *C.struct_jpeg_decompress_struct) (*C.jvirt_barray_ptr, error) {
result := C.jpeg_read_coefficients(dinfo)
if result == nil {
return nil, errors.New(jpegErrorMessage(unsafe.Pointer(dinfo)))
}
return result, nil
}

func startDecompress(dinfo *C.struct_jpeg_decompress_struct) error {
if C.start_decompress(dinfo) != 0 {
return errors.New(jpegErrorMessage(unsafe.Pointer(dinfo)))
Expand Down
2 changes: 1 addition & 1 deletion jpeg/destinationManager.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ package jpeg
/*
#include <stdlib.h>
#include <stdio.h>
#include <jpeglib.h>
#include "jpeglib.h"

// exported from golang
void destinationInit(struct jpeg_compress_struct*);
Expand Down
166 changes: 166 additions & 0 deletions jpeg/exif.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <setjmp.h>
#include "jpeglib.h"
#include "exif.h"

#define EXIF_JPEG_MARKER JPEG_APP0+1
#define EXIF_IDENT_STRING "Exif\000\000"

#define G_LITTLE_ENDIAN 1234
#define G_BIG_ENDIAN 4321

typedef unsigned int uint;
typedef unsigned short ushort;

const char leth[] = {0x49, 0x49, 0x2a, 0x00}; // Little endian TIFF header
const char beth[] = {0x4d, 0x4d, 0x00, 0x2a}; // Big endian TIFF header

LOCAL( unsigned short )
de_get16( void * ptr, uint endian ) {
unsigned char *bytes = (unsigned char *)(ptr);
if ( endian == G_BIG_ENDIAN )
{
return bytes[1] + (((uint)bytes[0]) << 8);
}
return bytes[0] + (((uint)bytes[1]) << 8);
}

LOCAL( unsigned int )
de_get32( void * ptr, uint endian ) {
unsigned char *bytes = (unsigned char *)(ptr);
if ( endian == G_BIG_ENDIAN )
{
return bytes[3] + (((uint)bytes[2]) << 8) + (((uint)bytes[1]) << 16) + (((uint)bytes[0]) << 24);
}
return bytes[0] + (((uint)bytes[1]) << 8) + (((uint)bytes[2]) << 16) + (((uint)bytes[3]) << 24);
}

int jpegtran_get_orientation (j_decompress_ptr cinfo)
{
/* This function looks through the meta data in the libjpeg decompress structure to
determine if an EXIF Orientation tag is present and if so return its value (1-8).
If no EXIF Orientation tag is found 0 (zero) is returned. */

uint i; /* index into working buffer */
uint orient_tag_id; /* endianed version of orientation tag ID */
uint ret; /* Return value */
uint offset; /* de-endianed offset in various situations */
uint tags; /* number of tags in current ifd */
uint tag; /* de-endianed tag */
uint type; /* de-endianed type of tag used as index into types[] */
uint count; /* de-endianed count of elements in a tag */
uint tiff = 0; /* offset to active tiff header */
uint endian = 0; /* detected endian of data */

jpeg_saved_marker_ptr exif_marker; /* Location of the Exif APP1 marker */
jpeg_saved_marker_ptr cmarker; /* Location to check for Exif APP1 marker */

/* check for Exif marker (also called the APP1 marker) */
exif_marker = NULL;
cmarker = cinfo->marker_list;
while (cmarker) {
if (cmarker->marker == EXIF_JPEG_MARKER) {
/* The Exif APP1 marker should contain a unique
identification string ("Exif\0\0"). Check for it. */
if (!memcmp (cmarker->data, EXIF_IDENT_STRING, 6)) {
exif_marker = cmarker;
}
}
cmarker = cmarker->next;
}
/* Did we find the Exif APP1 marker? */
if (exif_marker == NULL)
return 0;
/* Do we have enough data? */
if (exif_marker->data_length < 32)
return 0;

/* Check for TIFF header and catch endianess */
i = 0;

/* Just skip data until TIFF header - it should be within 16 bytes from marker start.
Normal structure relative to APP1 marker -
0x0000: APP1 marker entry = 2 bytes
0x0002: APP1 length entry = 2 bytes
0x0004: Exif Identifier entry = 6 bytes
0x000A: Start of TIFF header (Byte order entry) - 4 bytes
- This is what we look for, to determine endianess.
0x000E: 0th IFD offset pointer - 4 bytes

exif_marker->data points to the first data after the APP1 marker
and length entries, which is the exif identification string.
The TIFF header should thus normally be found at i=6, below,
and the pointer to IFD0 will be at 6+4 = 10.
*/

while (i < 16) {

/* Little endian TIFF header */
if (memcmp (&exif_marker->data[i], leth, 4) == 0){
endian = G_LITTLE_ENDIAN;
}

/* Big endian TIFF header */
else if (memcmp (&exif_marker->data[i], beth, 4) == 0){
endian = G_BIG_ENDIAN;
}

/* Keep looking through buffer */
else {
i++;
continue;
}
/* We have found either big or little endian TIFF header */
tiff = i;
break;
}

/* So did we find a TIFF header or did we just hit end of buffer? */
if (tiff == 0)
return 0;

/* Read out the offset pointer to IFD0 */
offset = de_get32(&exif_marker->data[i] + 4, endian);
i = i + offset;

/* Check that we still are within the buffer and can read the tag count */
if ((i + 2) > exif_marker->data_length)
return 0;

/* Find out how many tags we have in IFD0. As per the TIFF spec, the first
two bytes of the IFD contain a count of the number of tags. */
tags = de_get16(&exif_marker->data[i], endian);
i = i + 2;

/* Check that we still have enough data for all tags to check. The tags
are listed in consecutive 12-byte blocks. The tag ID, type, size, and
a pointer to the actual value, are packed into these 12 byte entries. */
if ((i + tags * 12) > exif_marker->data_length)
return 0;

/* Check through IFD0 for tags of interest */
while (tags--){
tag = de_get16(&exif_marker->data[i], endian);
type = de_get16(&exif_marker->data[i + 2], endian);
count = de_get32(&exif_marker->data[i + 4], endian);

/* Is this the orientation tag? */
if (tag == 0x112){

/* Check that type and count fields are OK. The orientation field
will consist of a single (count=1) 2-byte integer (type=3). */
if (type != 3 || count != 1) return 0;

/* Return the orientation value. Within the 12-byte block, the
pointer to the actual data is at offset 8. */
ret = de_get16(&exif_marker->data[i + 8], endian);
return ret <= 8 ? ret : 0;
}
/* move the pointer to the next 12-byte tag field. */
i = i + 12;
}

return 0; /* No EXIF Orientation tag found */
}
8 changes: 8 additions & 0 deletions jpeg/exif.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include "jpeglib.h"

int jpegtran_get_orientation (j_decompress_ptr cinfo);
2 changes: 1 addition & 1 deletion jpeg/jpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ package jpeg
#cgo LDFLAGS: -ljpeg
#include <stdlib.h>
#include <stdio.h>
#include <jpeglib.h>
#include "jpeglib.h"

static J_COLOR_SPACE getJCS_EXT_RGBA(void) {
#ifdef JCS_ALPHA_EXTENSIONS
Expand Down
145 changes: 145 additions & 0 deletions jpeg/jpegtran.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package jpeg

/*
#include <stdio.h>
#include <stdlib.h>
#include "jpeglib.h"
#include "transupp.h"
#include "exif.h"
*/
import "C"
import (
"errors"
"fmt"
"io"
)

type Transform int

// Constants numbers get from EXIF orientation
const (
TransfromUndefined Transform = 0
TransformNone Transform = 1
TransformFlipHorizontal Transform = 2
TransformRotate180 Transform = 3
TransformFlipVertical Transform = 4
TransformTranspose Transform = 5
TransformRotate90 Transform = 6
TransformTransverse Transform = 7
TransformRotate270 Transform = 8
)

type JpegTranOptions struct {
// Create progressive JPEG file
Progressive bool
// Fail if there is non-transformable edge blocks
Perfect bool
// Autorotate flag for automagically detect transformation by EXIF orientation
AutoRotate bool
// Image transformation
Transform Transform
}

// Create transform options for safe image processing
func NewJpegTranOptions() *JpegTranOptions {
return &JpegTranOptions{
AutoRotate: true,
Progressive: true,
Perfect: true,
}
}

//
// Based on https://github.com/cloudflare/jpegtran/blob/master/jpegtran.c implementation.
//
func JpegTran(r io.Reader, w io.Writer, options *JpegTranOptions) error {
if options == nil {
options = NewJpegTranOptions()
}

srcInfo := newDecompress(r)
if srcInfo == nil {
return errors.New("allocation failed")
}
defer destroyDecompress(srcInfo)

dstInfo, err := newCompress(w)
if err != nil {
return err
}
defer destroyCompress(dstInfo)

C.jcopy_markers_setup(srcInfo, C.JCOPYOPT_ALL)

err = readHeader(srcInfo)
if err != nil {
return err
}

var transformOption C.jpeg_transform_info
if options.Perfect {
transformOption.perfect = 1
}

if options.AutoRotate {
orientation := C.jpegtran_get_orientation(srcInfo)
if orientation > 0 && orientation <= 8 {
options.Transform = Transform(orientation)
} else {
options.Transform = TransfromUndefined
}
}

switch options.Transform {
case TransformNone, TransfromUndefined:
transformOption.transform = C.JXFORM_NONE
case TransformFlipHorizontal:
transformOption.transform = C.JXFORM_FLIP_H
case TransformFlipVertical:
transformOption.transform = C.JXFORM_FLIP_V
case TransformTranspose:
transformOption.transform = C.JXFORM_TRANSPOSE
case TransformTransverse:
transformOption.transform = C.JXFORM_TRANSVERSE
case TransformRotate90:
transformOption.transform = C.JXFORM_ROT_90
case TransformRotate180:
transformOption.transform = C.JXFORM_ROT_180
case TransformRotate270:
transformOption.transform = C.JXFORM_ROT_270
default:
return errors.New(fmt.Sprintf("unknown transform: %v", options.Transform))
}

//transformOption.transform = C.JXFORM_FLIP_H
if C.jtransform_request_workspace(srcInfo, &transformOption) == 0 {
return errors.New("transformation is not perfect")
}

srcCoefArrays, err := readCoefficients(srcInfo)
if err != nil {
return err
}

C.jpeg_copy_critical_parameters(srcInfo, dstInfo)

if options.Progressive {
C.jpeg_simple_progression(dstInfo)
}

dstCoefArrays := C.jtransform_adjust_parameters(srcInfo, dstInfo, srcCoefArrays, &transformOption)

if err := writeCoefficients(dstInfo, dstCoefArrays); err != nil {
return err
}

C.jtransform_execute_transform(srcInfo, dstInfo,
srcCoefArrays,
&transformOption)

if err := finishCompress(dstInfo); err != nil {
return err
}

return nil
}
Loading