Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
naqvis committed Jun 30, 2022
0 parents commit adb3c42
Show file tree
Hide file tree
Showing 66 changed files with 19,561 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: crystal-vips CI
on:
push:
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 6' # Every Saturday 6 AM
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Download source
uses: actions/checkout@v2
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
- name: Install libvips
env:
DEBIAN_FRONTEND: noninteractive
run:
# we only need the library
sudo apt-get install --fix-missing -qq -o Acquire::Retries=3 libvips-dev
- name: Run tests
run: crystal spec
- name: Check formatting
run: crystal tool format --check
25 changes: 25 additions & 0 deletions .github/workflows/doc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Deploy Docs

on:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Download source
uses: actions/checkout@v2
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
- name: Build
run: crystal docs
- name: Deploy
if: github.ref == 'refs/heads/main'
uses: JamesIves/[email protected]
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: docs
CLEAN: true
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/docs/
/lib/
/bin/
/images/
/.shards/
*.dwarf
.vscode/**
# Libraries don't need dependency lock
# Dependencies will be locked in applications that use them
/shard.lock
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Ali Naqvi <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
127 changes: 127 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# CrystalVips

[![crystal-vips CI](https://github.com/naqvis/crystal-vips/actions/workflows/ci.yml/badge.svg)](https://github.com/naqvis/crystal-vips/actions/workflows/ci.yml)
[![Latest release](https://img.shields.io/github/release/naqvis/crystal-vips.svg)](https://github.com/naqvis/crystal-vips/releases)
[![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://naqvis.github.io/crystal-vips/)

Provides Crystal language interface to the [libvips](https://github.com/libvips/libvips) image processing library.
Programs that use `CrystalVips` don't manipulate images directly, instead they create pipelines of image processing operations starting from a source image. When the pipe is connected to a destination, the whole pipeline executes at once and in parallel, streaming the image from source to destination in a set of small fragments.

Because `CrystalVips` is parallel, its' quick, and because it doesn't need to keep entire images in memory, its light. For example, the libvips speed and memory use benchmark:

[https://github.com/libvips/libvips/wiki/Speed-and-memory-use](https://github.com/libvips/libvips/wiki/Speed-and-memory-use)

## Pre-requisites

You need to [install the libvips
library](https://www.libvips.org/install.html). It's in the linux package managers, homebrew and MacPorts, and there are Windows binaries on the vips website. For example, on Debian:

```
sudo apt-get install --no-install-recommends libvips42
```

(`--no-install-recommends` stops Debian installing a *lot* of extra packages)

Or macOS:

```
brew install vips
```

## Installation

1. Add the dependency to your `shard.yml`:

```yaml
dependencies:
vips:
github: naqvis/crystal-vips
```
2. Run `shards install`

## Usage

```crystal
require "vips"
im = Vips::Image.new_from_file("image.jpg")
# put im at position (100, 100) in a 3000 x 3000 pixel image,
# make the other pixels in the image by mirroring im up / down /
# left / right, see
# https://libvips.github.io/libvips/API/current/libvips-conversion.html#vips-embed
im = im.embed(100, 100, 3000, 3000, extend: Vips::Enums::Extend::Mirror)
# multiply the green (middle) band by 2, leave the other two alone
im *= [1, 2, 1]
# make an image from an array constant, convolve with it
mask = Vips::Image.new_from_array([
[-1, -1, -1],
[-1, 16, -1],
[-1, -1, -1]], 8)
im = im.conv(mask, precision: Vips::Enums::Precision::Integer)
# finally, write the result back to a file on disk
im.write_to_file("output.jpg")
```

Refer to [samples](samples) folder for more samples

## Development

To run all tests:

```
crystal spec
```

# Getting more help

The libvips website has a handy table of [all the libvips
operators](http://libvips.github.io/libvips/API/current/func-list.html). Each
one links to the main API docs so you can see what you need to pass to it.

A simple way to see the arguments for an operation is to try running it
from the command-line. For example:

```bash
$ vips embed
embed an image in a larger image
usage:
embed in out x y width height
where:
in - Input image, input VipsImage
out - Output image, output VipsImage
x - Left edge of input in output, input gint
default: 0
min: -1000000000, max: 1000000000
y - Top edge of input in output, input gint
default: 0
min: -1000000000, max: 1000000000
width - Image width in pixels, input gint
default: 1
min: 1, max: 1000000000
height - Image height in pixels, input gint
default: 1
min: 1, max: 1000000000
optional arguments:
extend - How to generate the extra pixels, input VipsExtend
default: black
allowed: black, copy, repeat, mirror, white, background
background - Color for background pixels, input VipsArrayDouble
operation flags: sequential
```

## Contributing

1. Fork it (<https://github.com/naqvis/crystal-vips/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

## Contributors

- [Ali Naqvi](https://github.com/naqvis) - creator and maintainer
20 changes: 20 additions & 0 deletions example/annotate.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require "../src/vips"

if ARGV.size < 2
puts "Usage: #{__FILE__} input_image output"
exit(1)
end

im = Vips::Image.new_from_file(ARGV[0], access: Vips::Enums::Access::Sequential)

left_text, _ = Vips::Image.text("left corner", dpi: 300)
left = left_text.embed(50, 50, im.width, 150)

right_text, _ = Vips::Image.text("right corner", dpi: 300)
right = right_text.embed(im.width - right_text.width - 50, 50, im.width, 150)

footer = (left | right).ifthenelse(0, [255, 0, 0], blend: true)

im = im.insert(footer, 0, im.height, expand: true)

im.write_to_file ARGV[1]
22 changes: 22 additions & 0 deletions example/canny.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require "../src/vips"

# Edge detection

if ARGV.size < 2
puts "Usage: #{__FILE__} input_image output"
exit(1)
end

im = Vips::Image.new_from_file(ARGV[0], access: Vips::Enums::Access::Sequential)

# optionall convert to greyscale
# mono = im.colourspace(Vips::Enums::Interpretation::Bw)
# canny = mono.canny(sigma: 1.4, precision: Vips::Enums::Precision::Integer)

# Canny edge detector
canny = im.canny(sigma: 1.4, precision: Vips::Enums::Precision::Integer)

# Canny makes a float image, scale the output up to make it visible
scale = canny * 64

scale.write_to_file(ARGV[1])
83 changes: 83 additions & 0 deletions example/captcha_generator.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
require "../src/vips"

# Captcha generator
# Reference: https://github.com/libvips/libvips/issues/898

def wobble(image)
# a warp image is a 2D grid containing the new coordinates of each pixel with
# the new x in band 0 and the new y in band 1
#
# you can also use a complex image
#
# start from a low-res XY image and distort it

xy = Vips::Image.xyz(image.width // 20, image.height // 20)
x_distort = Vips::Image.gaussnoise(xy.width, xy.height)
y_distort = Vips::Image.gaussnoise(xy.width, xy.height)
xy += (x_distort.bandjoin(y_distort) / 150)
xy = xy.resize(20)
xy *= 20

# apply the warp
image.mapim(xy)
end

if ARGV.size < 2
puts "Usage: #{__FILE__} outputfile text"
exit(1)
end

text_layer = Vips::Image.black 1, 1
x_position = 0

ARGV[1].each_char do |c|
if c.ascii_whitespace?
x_position += 50
next
end

letter, _ = Vips::Image.text(c.to_s, dpi: 600)

image = letter.gravity(Vips::Enums::CompassDirection::Centre, letter.width + 50, letter.height + 50)

# random scale and rotate
image = image.similarity(scale: Random.rand(0.2) + 0.8,
angle: Random.rand(40) - 20)

# random wobble
image = wobble(image)

# random color
color = (1..3).map { Random.rand(255) }
image = image.ifthenelse(color, 0, true)

# tag as 9-bit srgb
image = image.copy(interpretation: Vips::Enums::Interpretation::Srgb).cast(Vips::Enums::BandFormat::Uchar)

# position at our write position in the image
image = image.embed(x_position, 0, image.width + x_position, image.height)

text_layer += image
text_layer = text_layer.cast(Vips::Enums::BandFormat::Uchar)

x_position += letter.width
end

# remove any unused edges
text_layer = text_layer.crop(*text_layer.find_trim(background: 0))

# make an alpha for the text layer: just a mono version of the image, but scaled
# up so letters themeselves are not transparent
alpha = (text_layer.colourspace(Vips::Enums::Interpretation::Bw) * 3).cast(Vips::Enums::BandFormat::Uchar)
text_layer = text_layer.bandjoin(alpha)

# make a white background with random speckles
speckles = Vips::Image.gaussnoise(text_layer.width, text_layer.height, mean: 400, sigma: 200)
background = (1..3).reduce(speckles) do |a, _|
a.bandjoin(Vips::Image.gaussnoise(text_layer.width, text_layer.height, mean: 400, sigma: 200))
.copy(interpretation: Vips::Enums::Interpretation::Srgb).cast(Vips::Enums::BandFormat::Uchar)
end

# composite the text over the background
final = background.composite(text_layer, Vips::Enums::BlendMode::Over)
final.write_to_file ARGV[0]
32 changes: 32 additions & 0 deletions example/combine.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require "../src/vips"

# Conversion
# Reference: https://github.com/libvips/lua-vips/blob/master/example/combine.lua

if ARGV.size < 3
puts "Usage: #{__FILE__} input_image watermark_image output"
exit(1)
end

LEFT = 100
TOP = 100

im = Vips::Image.new_from_file(ARGV[0], access: Vips::Enums::Access::Sequential)
wm = Vips::Image.new_from_file(ARGV[1], access: Vips::Enums::Access::Sequential)

width = wm.width
height = wm.height

# extract related area from main image
base = im.crop(LEFT, TOP, width, height)

# composite the two areas using the PDF "over" mode
composite = base.composite(wm, Vips::Enums::BlendMode::Over)

# the result will have an alpha, and our base image does not .. we must flatten
# out the alpha before we can insert it back into a plain RGB JPG image
rgb = composite.flatten

# insert composite back in to main image on related area
combined = im.insert(rgb, LEFT, TOP)
combined.write_to_file(ARGV[2])
Loading

0 comments on commit adb3c42

Please sign in to comment.