Skip to content
This repository was archived by the owner on Oct 8, 2025. It is now read-only.
Merged
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
53 changes: 50 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Halfshell uses a JSON file for configuration. An example is shown below:
"processors": {
"default": {
"image_compression_quality": 85,
"maintain_aspect_ratio": true,
"scale_mode": "aspect_fit",
"max_blur_radius_percentage": 0,
"max_image_height": 0,
"max_image_width": 1000
Expand Down Expand Up @@ -87,7 +87,7 @@ This will start the server on port 8080, and service requests whose path begins
http://localhost:8080/users/joe/default.jpg?w=100&h=100
http://localhost:8080/blog/posts/announcement.jpg?w=600&h=200

The image_host named group in the route pattern match (e.g., `^/users(?P<image_path>/.*)$`) gets extracted as the request path for the source. In this instance, the file “joe/default.jpg” is requested from the “my-company-profile-photos” S3 bucket. The processor resizes the image to a width and height of 100. Since the maintain_aspect_ratio setting is set to true, the image will have a maximum width and height of 100, but may be smaller in one dimension in order to maintain the aspect ratio.
The image_host named group in the route pattern match (e.g., `^/users(?P<image_path>/.*)$`) gets extracted as the request path for the source. In this instance, the file “joe/default.jpg” is requested from the “my-company-profile-photos” S3 bucket. The processor resizes the image to a width and height of 100.

### Server

Expand Down Expand Up @@ -141,10 +141,57 @@ The compression quality to use for JPEG images.

##### maintain_aspect_ratio
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a DEPRECATED warning here instead? Also document crop_mode.


DEPRECATED: Use the `aspect_fit` `scale_mode` instead.

If this is set to true, the resized images will always maintain the original
aspect ratio. When set to false, the image will be stretched to fit the width
and height requested.

##### scale_mode

When changing the dimensions of an image, you may want to crop edges or
constrain proportions. Use the `scale_mode` setting to define these rules.

A value of `aspect_fit` will change the image size to fit in the given
dimensions while retaining original proportions. No part of the image will be
cut away.

A value of `aspect_fill` will change the image size to at least fit the given
dimensions while retaining original proportions. No part of the image will be
cut away.

A value of `aspect_crop` will change the image size to fit in the given
dimensions while retaining original proportions. Edges that do not fit in the
given dimensions will be cut off.

The default behavior is to `fill`, which changes the image size to fit the given
dimensions and will NOT retain the original proportions.

Cheat Sheet:

Image dimensions: 500x800
Requested dimensions: 400x400

Scale mode: fill
New dimensions: 400x400
Maintain aspect ratio: NO
Cropping: NO

Scale mode: aspect_fit
New dimensions: 250x400
Maintain aspect ratio: YES
Cropping: NO

Scale mode: aspect_fill
New dimensions: 400x640
Maintain aspect ratio: YES
Cropping: NO

Scale mode: aspect_crop
New dimensions: 400x400
Maintain aspect ratio: YES
Cropping: YES

##### default_image_width

In the absence of a width parameter in the request, use this as image width. A
Expand Down Expand Up @@ -204,4 +251,4 @@ Run `make format` before sending any pull requests.

### Questions?

File an issue or send an email to rafik@oysterbooks.com.
File an issue or send an email to rafik@oysterbooks.com.
2 changes: 1 addition & 1 deletion examples/filesystem_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"processors": {
"default": {
"image_compression_quality": 85,
"maintain_aspect_ratio": true,
"scale_mode": "aspect_fit",
"max_blur_radius_percentage": 0,
"max_image_height": 0,
"max_image_width": 1000
Expand Down
2 changes: 1 addition & 1 deletion examples/s3_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"processors": {
"default": {
"image_compression_quality": 85,
"maintain_aspect_ratio": true,
"scale_mode": "aspect_fill",
"max_blur_radius_percentage": 0,
"max_image_height": 0,
"max_image_width": 1000
Expand Down
35 changes: 28 additions & 7 deletions halfshell/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,14 @@ type SourceConfig struct {
type ProcessorConfig struct {
Name string
ImageCompressionQuality uint64
MaintainAspectRatio bool
DefaultScaleMode uint
DefaultImageHeight uint64
DefaultImageWidth uint64
MaxImageHeight uint64
MaxImageWidth uint64
MaxImageDimensions ImageDimensions
MaxBlurRadiusPercentage float64

// DEPRECATED
MaintainAspectRatio bool
}

// StatterConfig holds configuration data for StatsD
Expand Down Expand Up @@ -207,16 +209,35 @@ func (c *configParser) parseSourceConfig(sourceName string) *SourceConfig {
}

func (c *configParser) parseProcessorConfig(processorName string) *ProcessorConfig {
return &ProcessorConfig{
scaleModeName := c.stringForKeypath("processors.%s.default_scale_mode", processorName)
scaleMode, _ := ScaleModes[scaleModeName]
if scaleMode == 0 {
scaleMode = ScaleFill
}

maxDimensions := ImageDimensions{
Width: uint(c.uintForKeypath("processors.%s.max_image_width", processorName)),
Height: uint(c.uintForKeypath("processors.%s.max_image_height", processorName)),
}

config := &ProcessorConfig{
Name: processorName,
ImageCompressionQuality: c.uintForKeypath("processors.%s.image_compression_quality", processorName),
MaintainAspectRatio: c.boolForKeypath("processors.%s.maintain_aspect_ratio", processorName),
DefaultScaleMode: scaleMode,
DefaultImageHeight: c.uintForKeypath("processors.%s.default_image_height", processorName),
DefaultImageWidth: c.uintForKeypath("processors.%s.default_image_width", processorName),
MaxImageHeight: c.uintForKeypath("processors.%s.max_image_height", processorName),
MaxImageWidth: c.uintForKeypath("processors.%s.max_image_width", processorName),
MaxImageDimensions: maxDimensions,
MaxBlurRadiusPercentage: c.floatForKeypath("processors.%s.max_blur_radius_percentage", processorName),

// DEPRECATED
MaintainAspectRatio: c.boolForKeypath("processors.%s.maintain_aspect_ratio", processorName),
}

if config.MaintainAspectRatio {
config.DefaultScaleMode = ScaleAspectFit
}

return config
}

func (c *configParser) valueForKeypath(valueType reflect.Kind, keypathFormat string, v ...interface{}) interface{} {
Expand Down
89 changes: 59 additions & 30 deletions halfshell/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,61 +22,90 @@ package halfshell

import (
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/rafikk/imagick/imagick"
)

// Image contains a byte array of the image data and its MIME type.
// TODO: See if we can use the std library's Image type without incurring
// the hit of extra copying.
var EmptyImageDimensions = ImageDimensions{}
var EmptyResizeDimensions = ResizeDimensions{}

type Image struct {
Bytes []byte
MimeType string
Wand *imagick.MagickWand
Signature string
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this value is no longer used.

destroyed bool
}

// NewImageFromHTTPResponse returns a pointer to a new Image created from an
// HTTP response object.
func NewImageFromHTTPResponse(httpResponse *http.Response) (*Image, error) {
imageBytes, err := ioutil.ReadAll(httpResponse.Body)
defer httpResponse.Body.Close()
func NewImageFromBuffer(buffer io.Reader) (image *Image, err error) {
bytes, err := ioutil.ReadAll(buffer)
if err != nil {
return nil, err
}

return &Image{
Bytes: imageBytes,
MimeType: httpResponse.Header.Get("Content-Type"),
}, nil
}

// NewImageFromFile returns a pointer to a new Image created from a file.
func NewImageFromFile(file *os.File) (*Image, error) {
imageBytes, err := ioutil.ReadAll(file)
image = &Image{Wand: imagick.NewMagickWand()}
err = image.Wand.ReadImageBlob(bytes)
if err != nil {
return nil, err
}

return &Image{
Bytes: imageBytes,
MimeType: mime.TypeByExtension(filepath.Ext(file.Name())),
}, nil
return image, nil
}

func NewImageFromFile(file *os.File) (image *Image, err error) {
image, err = NewImageFromBuffer(file)
return image, err
}

func (i *Image) GetMIMEType() string {
return fmt.Sprintf("image/%s", strings.ToLower(i.Wand.GetImageFormat()))
}

func (i *Image) GetBytes() (bytes []byte, size int) {
bytes = i.Wand.GetImageBlob()
size = len(bytes)
return bytes, size
}

func (i *Image) GetWidth() uint {
return i.Wand.GetImageWidth()
}

func (i *Image) GetHeight() uint {
return i.Wand.GetImageHeight()
}

func (i *Image) GetDimensions() ImageDimensions {
return ImageDimensions{i.GetWidth(), i.GetHeight()}
}

func (i *Image) GetSignature() string {
return i.Wand.GetImageSignature()
}

func (i *Image) Destroy() {
if !i.destroyed {
i.Wand.Destroy()
i.destroyed = true
}
}

// ImageDimensions is the width and height of an image.
type ImageDimensions struct {
Width uint64
Height uint64
Width uint
Height uint
}

// AspectRatio returns the image dimension's aspect ratio.
func (d ImageDimensions) AspectRatio() float64 {
return float64(d.Width) / float64(d.Height)
}

func (d ImageDimensions) String() string {
return fmt.Sprintf("%dx%d", d.Width, d.Height)
}

type ResizeDimensions struct {
Scale ImageDimensions
Crop ImageDimensions
}
Loading