mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
Add quality and compressionLevel options for output image. #24
This commit is contained in:
parent
8322b442e0
commit
f8338e7c4f
16
README.md
16
README.md
@ -94,11 +94,11 @@ sharp('input.jpg').resize(null, 200).progressive().toBuffer(function(err, buffer
|
|||||||
```
|
```
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
sharp('input.png').resize(300).sharpen().webp(function(err, buffer) {
|
sharp('input.png').resize(300).sharpen().quality(90).webp(function(err, buffer) {
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
// buffer contains sharpened WebP image data (converted from PNG), 300 pixels wide
|
// buffer contains 300 pixels wide, sharpened, 90% quality WebP image data
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -159,6 +159,18 @@ Perform a mild sharpen of the resultant image. This typically reduces performanc
|
|||||||
|
|
||||||
Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed.
|
Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed.
|
||||||
|
|
||||||
|
### quality(quality)
|
||||||
|
|
||||||
|
The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`.
|
||||||
|
|
||||||
|
`quality` is a Number between 1 and 100.
|
||||||
|
|
||||||
|
### compressionLevel(compressionLevel)
|
||||||
|
|
||||||
|
An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`.
|
||||||
|
|
||||||
|
`compressionLevel` is a Number between -1 and 9.
|
||||||
|
|
||||||
### sequentialRead()
|
### sequentialRead()
|
||||||
|
|
||||||
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
|
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
|
||||||
|
278
index.js
278
index.js
@ -1,128 +1,150 @@
|
|||||||
/*jslint node: true */
|
/*jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var sharp = require('./build/Release/sharp');
|
var sharp = require('./build/Release/sharp');
|
||||||
|
|
||||||
var Sharp = function(input) {
|
var Sharp = function(input) {
|
||||||
if (!(this instanceof Sharp)) {
|
if (!(this instanceof Sharp)) {
|
||||||
return new Sharp(input);
|
return new Sharp(input);
|
||||||
}
|
}
|
||||||
this.options = {
|
this.options = {
|
||||||
width: -1,
|
width: -1,
|
||||||
height: -1,
|
height: -1,
|
||||||
canvas: 'c',
|
canvas: 'c',
|
||||||
sharpen: false,
|
sharpen: false,
|
||||||
progressive: false,
|
progressive: false,
|
||||||
sequentialRead: false,
|
sequentialRead: false,
|
||||||
output: '__jpeg'
|
quality: 80,
|
||||||
};
|
compressionLevel: 6,
|
||||||
if (typeof input === 'string') {
|
output: '__jpeg'
|
||||||
this.options.inFile = input;
|
};
|
||||||
} else if (typeof input ==='object' && input instanceof Buffer) {
|
if (typeof input === 'string') {
|
||||||
this.options.inBuffer = input;
|
this.options.inFile = input;
|
||||||
} else {
|
} else if (typeof input ==='object' && input instanceof Buffer) {
|
||||||
throw 'Unsupported input ' + typeof input;
|
this.options.inBuffer = input;
|
||||||
}
|
} else {
|
||||||
return this;
|
throw 'Unsupported input ' + typeof input;
|
||||||
};
|
}
|
||||||
module.exports = Sharp;
|
return this;
|
||||||
|
};
|
||||||
Sharp.prototype.crop = function() {
|
module.exports = Sharp;
|
||||||
this.options.canvas = 'c';
|
|
||||||
return this;
|
Sharp.prototype.crop = function() {
|
||||||
};
|
this.options.canvas = 'c';
|
||||||
|
return this;
|
||||||
Sharp.prototype.embedWhite = function() {
|
};
|
||||||
this.options.canvas = 'w';
|
|
||||||
return this;
|
Sharp.prototype.embedWhite = function() {
|
||||||
};
|
this.options.canvas = 'w';
|
||||||
|
return this;
|
||||||
Sharp.prototype.embedBlack = function() {
|
};
|
||||||
this.options.canvas = 'b';
|
|
||||||
return this;
|
Sharp.prototype.embedBlack = function() {
|
||||||
};
|
this.options.canvas = 'b';
|
||||||
|
return this;
|
||||||
Sharp.prototype.sharpen = function(sharpen) {
|
};
|
||||||
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
|
|
||||||
return this;
|
Sharp.prototype.sharpen = function(sharpen) {
|
||||||
};
|
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
|
||||||
|
return this;
|
||||||
Sharp.prototype.progressive = function(progressive) {
|
};
|
||||||
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
|
|
||||||
return this;
|
Sharp.prototype.progressive = function(progressive) {
|
||||||
};
|
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
|
||||||
|
return this;
|
||||||
Sharp.prototype.sequentialRead = function(sequentialRead) {
|
};
|
||||||
this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true;
|
|
||||||
return this;
|
Sharp.prototype.sequentialRead = function(sequentialRead) {
|
||||||
};
|
this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true;
|
||||||
|
return this;
|
||||||
Sharp.prototype.resize = function(width, height) {
|
};
|
||||||
if (!width) {
|
|
||||||
this.options.width = -1;
|
Sharp.prototype.quality = function(quality) {
|
||||||
} else {
|
if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) {
|
||||||
if (!Number.isNaN(width)) {
|
this.options.quality = quality;
|
||||||
this.options.width = width;
|
} else {
|
||||||
} else {
|
throw 'Invalid quality (1 to 100) ' + quality;
|
||||||
throw 'Invalid width ' + width;
|
}
|
||||||
}
|
return this;
|
||||||
}
|
};
|
||||||
if (!height) {
|
|
||||||
this.options.height = -1;
|
Sharp.prototype.compressionLevel = function(compressionLevel) {
|
||||||
} else {
|
if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) {
|
||||||
if (!Number.isNaN(height)) {
|
this.options.compressionLevel = compressionLevel;
|
||||||
this.options.height = height;
|
} else {
|
||||||
} else {
|
throw 'Invalid compressionLevel (-1 to 9) ' + compressionLevel;
|
||||||
throw 'Invalid height ' + height;
|
}
|
||||||
}
|
return this;
|
||||||
}
|
};
|
||||||
return this;
|
|
||||||
};
|
Sharp.prototype.resize = function(width, height) {
|
||||||
|
if (!width) {
|
||||||
Sharp.prototype.write = function(output, callback) {
|
this.options.width = -1;
|
||||||
if (!output || output.length === 0) {
|
} else {
|
||||||
throw 'Invalid output';
|
if (!Number.isNaN(width)) {
|
||||||
} else {
|
this.options.width = width;
|
||||||
this._sharp(output, callback);
|
} else {
|
||||||
}
|
throw 'Invalid width ' + width;
|
||||||
return this;
|
}
|
||||||
};
|
}
|
||||||
|
if (!height) {
|
||||||
Sharp.prototype.toBuffer = function(callback) {
|
this.options.height = -1;
|
||||||
return this._sharp('__input', callback);
|
} else {
|
||||||
};
|
if (!Number.isNaN(height)) {
|
||||||
|
this.options.height = height;
|
||||||
Sharp.prototype.jpeg = function(callback) {
|
} else {
|
||||||
return this._sharp('__jpeg', callback);
|
throw 'Invalid height ' + height;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
Sharp.prototype.png = function(callback) {
|
return this;
|
||||||
return this._sharp('__png', callback);
|
};
|
||||||
};
|
|
||||||
|
Sharp.prototype.write = function(output, callback) {
|
||||||
Sharp.prototype.webp = function(callback) {
|
if (!output || output.length === 0) {
|
||||||
return this._sharp('__webp', callback);
|
throw 'Invalid output';
|
||||||
};
|
} else {
|
||||||
|
this._sharp(output, callback);
|
||||||
Sharp.prototype._sharp = function(output, callback) {
|
}
|
||||||
sharp.resize(
|
return this;
|
||||||
this.options.inFile,
|
};
|
||||||
this.options.inBuffer,
|
|
||||||
output,
|
Sharp.prototype.toBuffer = function(callback) {
|
||||||
this.options.width,
|
return this._sharp('__input', callback);
|
||||||
this.options.height,
|
};
|
||||||
this.options.canvas,
|
|
||||||
this.options.sharpen,
|
Sharp.prototype.jpeg = function(callback) {
|
||||||
this.options.progressive,
|
return this._sharp('__jpeg', callback);
|
||||||
this.options.sequentialRead,
|
};
|
||||||
callback
|
|
||||||
);
|
Sharp.prototype.png = function(callback) {
|
||||||
return this;
|
return this._sharp('__png', callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.cache = function(limit) {
|
Sharp.prototype.webp = function(callback) {
|
||||||
if (Number.isNaN(limit)) {
|
return this._sharp('__webp', callback);
|
||||||
limit = null;
|
};
|
||||||
}
|
|
||||||
return sharp.cache(limit);
|
Sharp.prototype._sharp = function(output, callback) {
|
||||||
};
|
sharp.resize(
|
||||||
|
this.options.inFile,
|
||||||
|
this.options.inBuffer,
|
||||||
|
output,
|
||||||
|
this.options.width,
|
||||||
|
this.options.height,
|
||||||
|
this.options.canvas,
|
||||||
|
this.options.sharpen,
|
||||||
|
this.options.progressive,
|
||||||
|
this.options.sequentialRead,
|
||||||
|
this.options.quality,
|
||||||
|
this.options.compressionLevel,
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.cache = function(limit) {
|
||||||
|
if (Number.isNaN(limit)) {
|
||||||
|
limit = null;
|
||||||
|
}
|
||||||
|
return sharp.cache(limit);
|
||||||
|
};
|
||||||
|
20
src/sharp.cc
20
src/sharp.cc
@ -24,6 +24,8 @@ struct resize_baton {
|
|||||||
bool sharpen;
|
bool sharpen;
|
||||||
bool progessive;
|
bool progessive;
|
||||||
VipsAccess access_method;
|
VipsAccess access_method;
|
||||||
|
int quality;
|
||||||
|
int compressionLevel;
|
||||||
std::string err;
|
std::string err;
|
||||||
|
|
||||||
resize_baton(): buffer_in_len(0), buffer_out_len(0) {}
|
resize_baton(): buffer_in_len(0), buffer_out_len(0) {}
|
||||||
@ -259,37 +261,37 @@ class ResizeWorker : public NanAsyncWorker {
|
|||||||
// Output
|
// Output
|
||||||
if (baton->file_out == "__jpeg" || (baton->file_out == "__input" && inputImageType == JPEG)) {
|
if (baton->file_out == "__jpeg" || (baton->file_out == "__input" && inputImageType == JPEG)) {
|
||||||
// Write JPEG to buffer
|
// Write JPEG to buffer
|
||||||
if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
|
if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
|
||||||
return resize_error(baton, sharpened);
|
return resize_error(baton, sharpened);
|
||||||
}
|
}
|
||||||
} else if (baton->file_out == "__png" || (baton->file_out == "__input" && inputImageType == PNG)) {
|
} else if (baton->file_out == "__png" || (baton->file_out == "__input" && inputImageType == PNG)) {
|
||||||
// Write PNG to buffer
|
// Write PNG to buffer
|
||||||
if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
|
if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progessive, NULL)) {
|
||||||
return resize_error(baton, sharpened);
|
return resize_error(baton, sharpened);
|
||||||
}
|
}
|
||||||
} else if (baton->file_out == "__webp" || (baton->file_out == "__input" && inputImageType == WEBP)) {
|
} else if (baton->file_out == "__webp" || (baton->file_out == "__input" && inputImageType == WEBP)) {
|
||||||
// Write WEBP to buffer
|
// Write WEBP to buffer
|
||||||
if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, NULL)) {
|
if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, NULL)) {
|
||||||
return resize_error(baton, sharpened);
|
return resize_error(baton, sharpened);
|
||||||
}
|
}
|
||||||
} else if (is_jpeg(baton->file_out)) {
|
} else if (is_jpeg(baton->file_out)) {
|
||||||
// Write JPEG to file
|
// Write JPEG to file
|
||||||
if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
|
if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
|
||||||
return resize_error(baton, sharpened);
|
return resize_error(baton, sharpened);
|
||||||
}
|
}
|
||||||
} else if (is_png(baton->file_out)) {
|
} else if (is_png(baton->file_out)) {
|
||||||
// Write PNG to file
|
// Write PNG to file
|
||||||
if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
|
if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progessive, NULL)) {
|
||||||
return resize_error(baton, sharpened);
|
return resize_error(baton, sharpened);
|
||||||
}
|
}
|
||||||
} else if (is_webp(baton->file_out)) {
|
} else if (is_webp(baton->file_out)) {
|
||||||
// Write WEBP to file
|
// Write WEBP to file
|
||||||
if (vips_webpsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, NULL)) {
|
if (vips_webpsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, NULL)) {
|
||||||
return resize_error(baton, sharpened);
|
return resize_error(baton, sharpened);
|
||||||
}
|
}
|
||||||
} else if (is_tiff(baton->file_out)) {
|
} else if (is_tiff(baton->file_out)) {
|
||||||
// Write TIFF to file
|
// Write TIFF to file
|
||||||
if (vips_tiffsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", 80, NULL)) {
|
if (vips_tiffsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
|
||||||
return resize_error(baton, sharpened);
|
return resize_error(baton, sharpened);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -345,8 +347,10 @@ NAN_METHOD(resize) {
|
|||||||
baton->sharpen = args[6]->BooleanValue();
|
baton->sharpen = args[6]->BooleanValue();
|
||||||
baton->progessive = args[7]->BooleanValue();
|
baton->progessive = args[7]->BooleanValue();
|
||||||
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
|
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
|
||||||
|
baton->quality = args[9]->Int32Value();
|
||||||
|
baton->compressionLevel = args[10]->Int32Value();
|
||||||
|
|
||||||
NanCallback *callback = new NanCallback(args[9].As<v8::Function>());
|
NanCallback *callback = new NanCallback(args[11].As<v8::Function>());
|
||||||
|
|
||||||
NanAsyncQueueWorker(new ResizeWorker(callback, baton));
|
NanAsyncQueueWorker(new ResizeWorker(callback, baton));
|
||||||
NanReturnUndefined();
|
NanReturnUndefined();
|
||||||
|
161
tests/unit.js
161
tests/unit.js
@ -1,73 +1,88 @@
|
|||||||
var sharp = require("../index");
|
var sharp = require("../index");
|
||||||
var path = require("path");
|
var path = require("path");
|
||||||
var imagemagick = require("imagemagick");
|
var imagemagick = require("imagemagick");
|
||||||
var assert = require("assert");
|
var assert = require("assert");
|
||||||
var async = require("async");
|
var async = require("async");
|
||||||
|
|
||||||
var fixturesPath = path.join(__dirname, "fixtures");
|
var fixturesPath = path.join(__dirname, "fixtures");
|
||||||
|
|
||||||
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
|
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
|
||||||
var outputJpg = path.join(fixturesPath, "output.jpg");
|
var outputJpg = path.join(fixturesPath, "output.jpg");
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
// Resize with exact crop
|
// Resize with exact crop
|
||||||
function(done) {
|
function(done) {
|
||||||
sharp(inputJpg).resize(320, 240).write(outputJpg, function(err) {
|
sharp(inputJpg).resize(320, 240).write(outputJpg, function(err) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
imagemagick.identify(outputJpg, function(err, features) {
|
imagemagick.identify(outputJpg, function(err, features) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(320, features.width);
|
assert.strictEqual(320, features.width);
|
||||||
assert.strictEqual(240, features.height);
|
assert.strictEqual(240, features.height);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Resize to fixed width
|
// Resize to fixed width
|
||||||
function(done) {
|
function(done) {
|
||||||
sharp(inputJpg).resize(320).write(outputJpg, function(err) {
|
sharp(inputJpg).resize(320).write(outputJpg, function(err) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
imagemagick.identify(outputJpg, function(err, features) {
|
imagemagick.identify(outputJpg, function(err, features) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(320, features.width);
|
assert.strictEqual(320, features.width);
|
||||||
assert.strictEqual(261, features.height);
|
assert.strictEqual(261, features.height);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Resize to fixed height
|
// Resize to fixed height
|
||||||
function(done) {
|
function(done) {
|
||||||
sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) {
|
sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
imagemagick.identify(outputJpg, function(err, features) {
|
imagemagick.identify(outputJpg, function(err, features) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(391, features.width);
|
assert.strictEqual(391, features.width);
|
||||||
assert.strictEqual(320, features.height);
|
assert.strictEqual(320, features.height);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Identity transform
|
// Identity transform
|
||||||
function(done) {
|
function(done) {
|
||||||
sharp(inputJpg).write(outputJpg, function(err) {
|
sharp(inputJpg).write(outputJpg, function(err) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
imagemagick.identify(outputJpg, function(err, features) {
|
imagemagick.identify(outputJpg, function(err, features) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(2725, features.width);
|
assert.strictEqual(2725, features.width);
|
||||||
assert.strictEqual(2225, features.height);
|
assert.strictEqual(2225, features.height);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Upscale
|
// Upscale
|
||||||
function(done) {
|
function(done) {
|
||||||
sharp(inputJpg).resize(3000).write(outputJpg, function(err) {
|
sharp(inputJpg).resize(3000).write(outputJpg, function(err) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
imagemagick.identify(outputJpg, function(err, features) {
|
imagemagick.identify(outputJpg, function(err, features) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
assert.strictEqual(3000, features.width);
|
assert.strictEqual(3000, features.width);
|
||||||
assert.strictEqual(2449, features.height);
|
assert.strictEqual(2449, features.height);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
]);
|
// Quality
|
||||||
|
function(done) {
|
||||||
|
sharp(inputJpg).resize(320, 240).quality(70).jpeg(function(err, buffer70) {
|
||||||
|
if (err) throw err;
|
||||||
|
sharp(inputJpg).resize(320, 240).jpeg(function(err, buffer80) {
|
||||||
|
if (err) throw err;
|
||||||
|
sharp(inputJpg).resize(320, 240).quality(90).jpeg(function(err, buffer90) {
|
||||||
|
assert(buffer70.length < buffer80.length);
|
||||||
|
assert(buffer80.length < buffer90.length);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
]);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user