Home Download Docs Code Community
     1	/*
     2	Copyright 2014 The Camlistore Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	// Package fastjpeg uses djpeg(1), from the Independent JPEG Group's
    18	// (www.ijg.org) jpeg package, to quickly down-sample images on load.  It can
    19	// sample images by a factor of 1, 2, 4 or 8.
    20	// This reduces the amount of data that must be decompressed into memory when
    21	// the full resolution image isn't required, i.e. in the case of generating
    22	// thumbnails.
    23	package fastjpeg // import "camlistore.org/pkg/images/fastjpeg"
    24	
    25	import (
    26		"bytes"
    27		"errors"
    28		"expvar"
    29		"fmt"
    30		"image"
    31		"image/color"
    32		_ "image/jpeg"
    33		"io"
    34		"log"
    35		"os"
    36		"os/exec"
    37		"strconv"
    38		"sync"
    39	
    40		"camlistore.org/pkg/buildinfo"
    41	
    42		"go4.org/readerutil"
    43	)
    44	
    45	var (
    46		ErrDjpegNotFound = errors.New("fastjpeg: djpeg not found in path")
    47	)
    48	
    49	// DjpegFailedError wraps errors returned when calling djpeg and handling its
    50	// response.  Used for type asserting and retrying with other jpeg decoders,
    51	// i.e. the standard library's jpeg.Decode.
    52	type DjpegFailedError struct {
    53		Err error
    54	}
    55	
    56	func (dfe DjpegFailedError) Error() string {
    57		return dfe.Err.Error()
    58	}
    59	
    60	// TODO(wathiede): do we need to conditionally add ".exe" on Windows? I have
    61	// no access to test on Windows.
    62	const djpegBin = "djpeg"
    63	
    64	var (
    65		checkAvailability sync.Once
    66		available         bool
    67	)
    68	
    69	var (
    70		djpegSuccessVar = expvar.NewInt("fastjpeg-djpeg-success")
    71		djpegFailureVar = expvar.NewInt("fastjpeg-djpeg-failure")
    72		// Bytes read from djpeg subprocess
    73		djpegBytesReadVar = expvar.NewInt("fastjpeg-djpeg-bytes-read")
    74		// Bytes written to djpeg subprocess
    75		djpegBytesWrittenVar = expvar.NewInt("fastjpeg-djpeg-bytes-written")
    76	)
    77	
    78	func Available() bool {
    79		checkAvailability.Do(func() {
    80			if ok, _ := strconv.ParseBool(os.Getenv("CAMLI_DISABLE_DJPEG")); ok {
    81				log.Println("CAMLI_DISABLE_DJPEG set in environment.  Disabling fastjpeg.")
    82				return
    83			}
    84	
    85			if p, err := exec.LookPath(djpegBin); p != "" && err == nil {
    86				available = true
    87				log.Printf("fastjpeg enabled with %s.", p)
    88			}
    89			if !available {
    90				log.Printf("%s not found in PATH, disabling fastjpeg.", djpegBin)
    91			}
    92		})
    93	
    94		return available
    95	}
    96	
    97	func init() {
    98		buildinfo.RegisterDjpegStatusFunc(djpegStatus)
    99	}
   100	
   101	func djpegStatus() string {
   102		// TODO: more info: its path, whether it works, its version, etc.
   103		if Available() {
   104			return "djpeg available"
   105		}
   106		return "djpeg optimization unavailable"
   107	}
   108	
   109	func readPNM(buf *bytes.Buffer) (image.Image, error) {
   110		var imgType, w, h int
   111		nTokens, err := fmt.Fscanf(buf, "P%d\n%d %d\n255\n", &imgType, &w, &h)
   112		if err != nil {
   113			return nil, err
   114		}
   115		if nTokens != 3 {
   116			hdr := buf.Bytes()
   117			if len(hdr) > 100 {
   118				hdr = hdr[:100]
   119			}
   120			return nil, fmt.Errorf("fastjpeg: Invalid PNM header: %q", hdr)
   121		}
   122	
   123		switch imgType {
   124		case 5: // Gray
   125			src := buf.Bytes()
   126			if len(src) != w*h {
   127				return nil, fmt.Errorf("fastjpeg: grayscale source buffer not sized w*h")
   128			}
   129			im := &image.Gray{
   130				Pix:    src,
   131				Stride: w,
   132				Rect:   image.Rect(0, 0, w, h),
   133			}
   134			return im, nil
   135		case 6: // RGB
   136			src := buf.Bytes()
   137			if len(src) != w*h*3 {
   138				return nil, fmt.Errorf("fastjpeg: RGB source buffer not sized w*h*3")
   139			}
   140			im := image.NewRGBA(image.Rect(0, 0, w, h))
   141			dst := im.Pix
   142			for i := 0; i < len(src)/3; i++ {
   143				dst[4*i+0] = src[3*i+0] // R
   144				dst[4*i+1] = src[3*i+1] // G
   145				dst[4*i+2] = src[3*i+2] // B
   146				dst[4*i+3] = 255        // Alpha
   147			}
   148			return im, nil
   149		default:
   150			return nil, fmt.Errorf("fastjpeg: Unsupported PNM type P%d", imgType)
   151		}
   152	}
   153	
   154	// Factor returns the sample factor DecodeSample should use to generate a
   155	// sampled image greater than or equal to sw x sh pixels given a source image
   156	// of w x h pixels.
   157	func Factor(w, h, sw, sh int) int {
   158		switch {
   159		case w>>3 >= sw && h>>3 >= sh:
   160			return 8
   161		case w>>2 >= sw && h>>2 >= sh:
   162			return 4
   163		case w>>1 >= sw && h>>1 >= sh:
   164			return 2
   165		}
   166		return 1
   167	}
   168	
   169	// DecodeDownsample decodes JPEG data in r, down-sampling it by factor.
   170	// If djpeg is not found, err is ErrDjpegNotFound and r is not read from.
   171	// If the execution of djpeg, or decoding the resulting PNM fails, error will
   172	// be of type DjpegFailedError.
   173	func DecodeDownsample(r io.Reader, factor int) (image.Image, error) {
   174		if !Available() {
   175			return nil, ErrDjpegNotFound
   176		}
   177		switch factor {
   178		case 1, 2, 4, 8:
   179		default:
   180			return nil, fmt.Errorf("fastjpeg: unsupported sample factor %d", factor)
   181		}
   182	
   183		buf := new(bytes.Buffer)
   184		tr := io.TeeReader(r, buf)
   185		ic, format, err := image.DecodeConfig(tr)
   186		if err != nil {
   187			return nil, err
   188		}
   189		if format != "jpeg" {
   190			return nil, fmt.Errorf("fastjpeg: Unsupported format %q", format)
   191		}
   192		var bpp int
   193		switch ic.ColorModel {
   194		case color.YCbCrModel:
   195			bpp = 4 // JPEG will decode to RGB, and we'll expand inplace to RGBA.
   196		case color.GrayModel:
   197			bpp = 1
   198		default:
   199			return nil, fmt.Errorf("fastjpeg: Unsupported thumnbnail color model %T", ic.ColorModel)
   200		}
   201		args := []string{djpegBin, "-scale", fmt.Sprintf("1/%d", factor)}
   202		cmd := exec.Command(args[0], args[1:]...)
   203		cmd.Stdin = readerutil.NewStatsReader(djpegBytesWrittenVar, io.MultiReader(buf, r))
   204	
   205		// Allocate space for the RGBA / Gray pixel data plus some extra for PNM
   206		// header info.  Explicitly allocate all the memory upfront to prevent
   207		// many smaller allocations.
   208		pixSize := ic.Width*ic.Height*bpp/factor/factor + 128
   209		w := bytes.NewBuffer(make([]byte, 0, pixSize))
   210		cmd.Stdout = w
   211	
   212		stderrW := new(bytes.Buffer)
   213		cmd.Stderr = stderrW
   214		if err := cmd.Run(); err != nil {
   215			djpegFailureVar.Add(1)
   216			return nil, DjpegFailedError{Err: fmt.Errorf("%v: %s", err, stderrW)}
   217		}
   218		djpegSuccessVar.Add(1)
   219		djpegBytesReadVar.Add(int64(w.Len()))
   220		m, err := readPNM(w)
   221		if err != nil {
   222			return m, DjpegFailedError{Err: err}
   223		}
   224		return m, nil
   225	}
Website layout inspired by git and memcached,
design done by up all day creative solutions.
Content by the authors.