SkPDF: Re-use Jpeg Image Shaders
Also, de-dup shader better. - SkBitmapKeyFromImage function factors out image de-dup code. - SkPDFUtils::ToBitmap funtion factors out to-bitmap code - make_image_shader function now takes Image rather than Bitmap. All tests render the same. Some are significantly smaller PDFs. Change-Id: Id8dd36b31c8e573f44a5e9f6058d5a1d622b6385 Reviewed-on: https://skia-review.googlesource.com/24081 Reviewed-by: Florin Malita <fmalita@chromium.org> Commit-Queue: Hal Canary <halcanary@google.com>
This commit is contained in:
parent
e46828675c
commit
4f29c20f20
@ -9,15 +9,19 @@
|
||||
|
||||
#include "SkImage_Base.h"
|
||||
|
||||
SkKeyedImage::SkKeyedImage(sk_sp<SkImage> i) : fImage(std::move(i)) {
|
||||
if (fImage) {
|
||||
if (const SkBitmap* bm = as_IB(fImage.get())->onPeekBitmap()) {
|
||||
SkIPoint o = bm->pixelRefOrigin();
|
||||
fKey = {fImage->bounds().makeOffset(o.fX, o.fY), bm->getGenerationID()};
|
||||
} else {
|
||||
fKey = {fImage->bounds(), fImage->uniqueID()};
|
||||
}
|
||||
SkBitmapKey SkBitmapKeyFromImage(const SkImage* image) {
|
||||
if (!image) {
|
||||
return {{0, 0, 0, 0}, 0};
|
||||
}
|
||||
if (const SkBitmap* bm = as_IB(image)->onPeekBitmap()) {
|
||||
SkIPoint o = bm->pixelRefOrigin();
|
||||
return {image->bounds().makeOffset(o.x(), o.y()), bm->getGenerationID()};
|
||||
}
|
||||
return {image->bounds(), image->uniqueID()};
|
||||
}
|
||||
|
||||
SkKeyedImage::SkKeyedImage(sk_sp<SkImage> i) : fImage(std::move(i)) {
|
||||
fKey = SkBitmapKeyFromImage(fImage.get());
|
||||
}
|
||||
|
||||
SkKeyedImage::SkKeyedImage(const SkBitmap& bm) : fImage(SkImage::MakeFromBitmap(bm)) {
|
||||
|
@ -37,4 +37,10 @@ private:
|
||||
sk_sp<SkImage> fImage;
|
||||
SkBitmapKey fKey = {{0, 0, 0, 0}, 0};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an Image, return the Bitmap Key that corresponds to it. If the Image
|
||||
* wraps a Bitmap, use that Bitmap's key.
|
||||
*/
|
||||
SkBitmapKey SkBitmapKeyFromImage(const SkImage*);
|
||||
#endif // SkKeyedImage_DEFINED
|
||||
|
@ -5,35 +5,27 @@
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "SkPDFBitmap.h"
|
||||
|
||||
#include "SkColorPriv.h"
|
||||
#include "SkData.h"
|
||||
#include "SkDeflate.h"
|
||||
#include "SkImage_Base.h"
|
||||
#include "SkImage.h"
|
||||
#include "SkJpegInfo.h"
|
||||
#include "SkPDFBitmap.h"
|
||||
#include "SkPDFCanon.h"
|
||||
#include "SkPDFTypes.h"
|
||||
#include "SkPDFUtils.h"
|
||||
#include "SkStream.h"
|
||||
#include "SkUnPreMultiply.h"
|
||||
|
||||
void image_get_ro_pixels(const SkImage* image, SkBitmap* dst) {
|
||||
SkColorSpace* legacyColorSpace = nullptr;
|
||||
if(as_IB(image)->getROPixels(dst, legacyColorSpace)
|
||||
&& dst->dimensions() == image->dimensions()) {
|
||||
return;
|
||||
}
|
||||
// no pixels or wrong size: fill with zeros.
|
||||
dst->setInfo(SkImageInfo::MakeN32(image->width(), image->height(), image->alphaType()));
|
||||
}
|
||||
|
||||
bool image_compute_is_opaque(const SkImage* image) {
|
||||
if (image->isOpaque()) {
|
||||
return true;
|
||||
}
|
||||
// keep output PDF small at cost of possible resource use.
|
||||
SkBitmap bm;
|
||||
image_get_ro_pixels(image, &bm);
|
||||
return SkBitmap::ComputeIsOpaque(bm);
|
||||
// if image can not be read, treat as transparent.
|
||||
return SkPDFUtils::ToBitmap(image, &bm) && SkBitmap::ComputeIsOpaque(bm);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@ -286,7 +278,10 @@ static void emit_image_xobject(SkWStream* stream,
|
||||
const sk_sp<SkPDFObject>& smask,
|
||||
const SkPDFObjNumMap& objNumMap) {
|
||||
SkBitmap bitmap;
|
||||
image_get_ro_pixels(image, &bitmap); // TODO(halcanary): test
|
||||
if (!SkPDFUtils::ToBitmap(image, &bitmap)) {
|
||||
// no pixels or wrong size: fill with zeros.
|
||||
bitmap.setInfo(SkImageInfo::MakeN32(image->width(), image->height(), image->alphaType()));
|
||||
}
|
||||
|
||||
// Write to a temporary buffer to get the compressed length.
|
||||
SkDynamicMemoryWStream buffer;
|
||||
@ -433,8 +428,7 @@ sk_sp<SkPDFObject> SkPDFCreateBitmapObject(sk_sp<SkImage> image,
|
||||
if (pixelSerializer) {
|
||||
SkBitmap bm;
|
||||
SkPixmap pmap;
|
||||
SkColorSpace* legacyColorSpace = nullptr;
|
||||
if (as_IB(image.get())->getROPixels(&bm, legacyColorSpace) && bm.peekPixels(&pmap)) {
|
||||
if (SkPDFUtils::ToBitmap(image.get(), &bm) && bm.peekPixels(&pmap)) {
|
||||
data = pixelSerializer->encodeToData(pmap);
|
||||
if (data && SkIsJFIF(data.get(), &info)) {
|
||||
bool yuv = info.fType == SkJFIFInfo::kYCbCr;
|
||||
|
@ -22,6 +22,12 @@
|
||||
#include "SkTemplates.h"
|
||||
|
||||
|
||||
static void draw_image_matrix(SkCanvas* canvas, const SkImage* img, const SkMatrix& matrix) {
|
||||
SkAutoCanvasRestore acr(canvas, true);
|
||||
canvas->concat(matrix);
|
||||
canvas->drawImage(img, 0, 0);
|
||||
}
|
||||
|
||||
static void draw_bitmap_matrix(SkCanvas* canvas, const SkBitmap& bm, const SkMatrix& matrix) {
|
||||
SkAutoCanvasRestore acr(canvas, true);
|
||||
canvas->concat(matrix);
|
||||
@ -30,9 +36,8 @@ static void draw_bitmap_matrix(SkCanvas* canvas, const SkBitmap& bm, const SkMat
|
||||
|
||||
static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
const SkPDFImageShaderKey& key,
|
||||
SkBitmap image) {
|
||||
SkASSERT(key.fBitmapKey ==
|
||||
(SkBitmapKey{image.getSubset(), image.getGenerationID()}));
|
||||
SkImage* image) {
|
||||
SkASSERT(image);
|
||||
|
||||
// The image shader pattern cell will be drawn into a separate device
|
||||
// in pattern cell space (no scaling on the bitmap, though there may be
|
||||
@ -42,14 +47,12 @@ static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
// to handle fake clamping.
|
||||
SkMatrix finalMatrix = key.fCanvasTransform;
|
||||
finalMatrix.preConcat(key.fShaderTransform);
|
||||
SkRect deviceBounds;
|
||||
deviceBounds.set(key.fBBox);
|
||||
SkRect deviceBounds = SkRect::Make(key.fBBox);
|
||||
if (!SkPDFUtils::InverseTransformBBox(finalMatrix, &deviceBounds)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SkRect bitmapBounds;
|
||||
image.getBounds(&bitmapBounds);
|
||||
SkRect bitmapBounds = SkRect::Make(image->bounds());
|
||||
|
||||
// For tiling modes, the bounds should be extended to include the bitmap,
|
||||
// otherwise the bitmap gets clipped out and the shader is empty and awful.
|
||||
@ -63,13 +66,12 @@ static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
deviceBounds.join(bitmapBounds);
|
||||
}
|
||||
|
||||
SkISize size = SkISize::Make(SkScalarRoundToInt(deviceBounds.width()),
|
||||
SkScalarRoundToInt(deviceBounds.height()));
|
||||
auto patternDevice = sk_make_sp<SkPDFDevice>(size, doc);
|
||||
SkISize patternDeviceSize = {SkScalarCeilToInt(deviceBounds.width()),
|
||||
SkScalarCeilToInt(deviceBounds.height())};
|
||||
auto patternDevice = sk_make_sp<SkPDFDevice>(patternDeviceSize, doc);
|
||||
SkCanvas canvas(patternDevice.get());
|
||||
|
||||
SkRect patternBBox;
|
||||
image.getBounds(&patternBBox);
|
||||
SkRect patternBBox = SkRect::Make(image->bounds());
|
||||
|
||||
// Translate the canvas so that the bitmap origin is at (0, 0).
|
||||
canvas.translate(-deviceBounds.left(), -deviceBounds.top());
|
||||
@ -80,24 +82,24 @@ static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
// If the bitmap is out of bounds (i.e. clamp mode where we only see the
|
||||
// stretched sides), canvas will clip this out and the extraneous data
|
||||
// won't be saved to the PDF.
|
||||
canvas.drawBitmap(image, 0, 0);
|
||||
canvas.drawImage(image, 0, 0);
|
||||
|
||||
SkScalar width = SkIntToScalar(image.width());
|
||||
SkScalar height = SkIntToScalar(image.height());
|
||||
SkScalar width = SkIntToScalar(image->width());
|
||||
SkScalar height = SkIntToScalar(image->height());
|
||||
|
||||
// Tiling is implied. First we handle mirroring.
|
||||
if (tileModes[0] == SkShader::kMirror_TileMode) {
|
||||
SkMatrix xMirror;
|
||||
xMirror.setScale(-1, 1);
|
||||
xMirror.postTranslate(2 * width, 0);
|
||||
draw_bitmap_matrix(&canvas, image, xMirror);
|
||||
draw_image_matrix(&canvas, image, xMirror);
|
||||
patternBBox.fRight += width;
|
||||
}
|
||||
if (tileModes[1] == SkShader::kMirror_TileMode) {
|
||||
SkMatrix yMirror;
|
||||
yMirror.setScale(SK_Scalar1, -SK_Scalar1);
|
||||
yMirror.postTranslate(0, 2 * height);
|
||||
draw_bitmap_matrix(&canvas, image, yMirror);
|
||||
draw_image_matrix(&canvas, image, yMirror);
|
||||
patternBBox.fBottom += height;
|
||||
}
|
||||
if (tileModes[0] == SkShader::kMirror_TileMode &&
|
||||
@ -105,53 +107,66 @@ static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
SkMatrix mirror;
|
||||
mirror.setScale(-1, -1);
|
||||
mirror.postTranslate(2 * width, 2 * height);
|
||||
draw_bitmap_matrix(&canvas, image, mirror);
|
||||
draw_image_matrix(&canvas, image, mirror);
|
||||
}
|
||||
|
||||
// Then handle Clamping, which requires expanding the pattern canvas to
|
||||
// cover the entire surfaceBBox.
|
||||
|
||||
SkBitmap bitmap;
|
||||
if (tileModes[0] == SkShader::kClamp_TileMode ||
|
||||
tileModes[1] == SkShader::kClamp_TileMode) {
|
||||
// For now, the easiest way to access the colors in the corners and sides is
|
||||
// to just make a bitmap from the image.
|
||||
if (!SkPDFUtils::ToBitmap(image, &bitmap)) {
|
||||
bitmap.allocN32Pixels(image->width(), image->height());
|
||||
bitmap.eraseColor(0x00000000);
|
||||
}
|
||||
}
|
||||
|
||||
// If both x and y are in clamp mode, we start by filling in the corners.
|
||||
// (Which are just a rectangles of the corner colors.)
|
||||
if (tileModes[0] == SkShader::kClamp_TileMode &&
|
||||
tileModes[1] == SkShader::kClamp_TileMode) {
|
||||
SkASSERT(!bitmap.drawsNothing());
|
||||
SkPaint paint;
|
||||
SkRect rect;
|
||||
rect = SkRect::MakeLTRB(deviceBounds.left(), deviceBounds.top(), 0, 0);
|
||||
if (!rect.isEmpty()) {
|
||||
paint.setColor(image.getColor(0, 0));
|
||||
paint.setColor(bitmap.getColor(0, 0));
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
|
||||
rect = SkRect::MakeLTRB(width, deviceBounds.top(),
|
||||
deviceBounds.right(), 0);
|
||||
if (!rect.isEmpty()) {
|
||||
paint.setColor(image.getColor(image.width() - 1, 0));
|
||||
paint.setColor(bitmap.getColor(bitmap.width() - 1, 0));
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
|
||||
rect = SkRect::MakeLTRB(width, height,
|
||||
deviceBounds.right(), deviceBounds.bottom());
|
||||
if (!rect.isEmpty()) {
|
||||
paint.setColor(image.getColor(image.width() - 1,
|
||||
image.height() - 1));
|
||||
paint.setColor(bitmap.getColor(bitmap.width() - 1,
|
||||
bitmap.height() - 1));
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
|
||||
rect = SkRect::MakeLTRB(deviceBounds.left(), height,
|
||||
0, deviceBounds.bottom());
|
||||
if (!rect.isEmpty()) {
|
||||
paint.setColor(image.getColor(0, image.height() - 1));
|
||||
paint.setColor(bitmap.getColor(0, bitmap.height() - 1));
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
}
|
||||
|
||||
// Then expand the left, right, top, then bottom.
|
||||
if (tileModes[0] == SkShader::kClamp_TileMode) {
|
||||
SkIRect subset = SkIRect::MakeXYWH(0, 0, 1, image.height());
|
||||
SkASSERT(!bitmap.drawsNothing());
|
||||
SkIRect subset = SkIRect::MakeXYWH(0, 0, 1, bitmap.height());
|
||||
if (deviceBounds.left() < 0) {
|
||||
SkBitmap left;
|
||||
SkAssertResult(image.extractSubset(&left, subset));
|
||||
SkAssertResult(bitmap.extractSubset(&left, subset));
|
||||
|
||||
SkMatrix leftMatrix;
|
||||
leftMatrix.setScale(-deviceBounds.left(), 1);
|
||||
@ -168,8 +183,8 @@ static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
|
||||
if (deviceBounds.right() > width) {
|
||||
SkBitmap right;
|
||||
subset.offset(image.width() - 1, 0);
|
||||
SkAssertResult(image.extractSubset(&right, subset));
|
||||
subset.offset(bitmap.width() - 1, 0);
|
||||
SkAssertResult(bitmap.extractSubset(&right, subset));
|
||||
|
||||
SkMatrix rightMatrix;
|
||||
rightMatrix.setScale(deviceBounds.right() - width, 1);
|
||||
@ -186,10 +201,11 @@ static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
}
|
||||
|
||||
if (tileModes[1] == SkShader::kClamp_TileMode) {
|
||||
SkIRect subset = SkIRect::MakeXYWH(0, 0, image.width(), 1);
|
||||
SkASSERT(!bitmap.drawsNothing());
|
||||
SkIRect subset = SkIRect::MakeXYWH(0, 0, bitmap.width(), 1);
|
||||
if (deviceBounds.top() < 0) {
|
||||
SkBitmap top;
|
||||
SkAssertResult(image.extractSubset(&top, subset));
|
||||
SkAssertResult(bitmap.extractSubset(&top, subset));
|
||||
|
||||
SkMatrix topMatrix;
|
||||
topMatrix.setScale(SK_Scalar1, -deviceBounds.top());
|
||||
@ -206,8 +222,8 @@ static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
|
||||
if (deviceBounds.bottom() > height) {
|
||||
SkBitmap bottom;
|
||||
subset.offset(0, image.height() - 1);
|
||||
SkAssertResult(image.extractSubset(&bottom, subset));
|
||||
subset.offset(0, bitmap.height() - 1);
|
||||
SkAssertResult(bitmap.extractSubset(&bottom, subset));
|
||||
|
||||
SkMatrix bottomMatrix;
|
||||
bottomMatrix.setScale(SK_Scalar1, deviceBounds.bottom() - height);
|
||||
@ -225,7 +241,7 @@ static sk_sp<SkPDFStream> make_image_shader(SkPDFDocument* doc,
|
||||
|
||||
auto imageShader = sk_make_sp<SkPDFStream>(patternDevice->content());
|
||||
SkPDFUtils::PopulateTilingPatternDict(imageShader->dict(), patternBBox,
|
||||
patternDevice->makeResourceDict(), finalMatrix);
|
||||
patternDevice->makeResourceDict(), finalMatrix);
|
||||
return imageShader;
|
||||
}
|
||||
|
||||
@ -245,7 +261,7 @@ static sk_sp<SkPDFObject> make_fallback_shader(SkPDFDocument* doc,
|
||||
canvasTransform,
|
||||
SkMatrix::I(),
|
||||
surfaceBBox,
|
||||
{{0, 0, 0, 0}, 0},
|
||||
{{0, 0, 0, 0}, 0}, // don't need the key; won't de-dup.
|
||||
{SkShader::kClamp_TileMode, SkShader::kClamp_TileMode}};
|
||||
|
||||
key.fShaderTransform = shader->getLocalMatrix();
|
||||
@ -271,23 +287,25 @@ static sk_sp<SkPDFObject> make_fallback_shader(SkPDFDocument* doc,
|
||||
SkSize scale = {SkIntToScalar(size.width()) / shaderRect.width(),
|
||||
SkIntToScalar(size.height()) / shaderRect.height()};
|
||||
|
||||
SkBitmap image;
|
||||
image.allocN32Pixels(size.width(), size.height());
|
||||
image.eraseColor(SK_ColorTRANSPARENT);
|
||||
SkBitmap bitmap;
|
||||
bitmap.allocN32Pixels(size.width(), size.height());
|
||||
bitmap.eraseColor(SK_ColorTRANSPARENT);
|
||||
|
||||
SkPaint p;
|
||||
p.setShader(sk_ref_sp(shader));
|
||||
|
||||
SkCanvas canvas(image);
|
||||
SkCanvas canvas(bitmap);
|
||||
canvas.scale(scale.width(), scale.height());
|
||||
canvas.translate(-shaderRect.x(), -shaderRect.y());
|
||||
canvas.drawPaint(p);
|
||||
|
||||
key.fShaderTransform.setTranslate(shaderRect.x(), shaderRect.y());
|
||||
key.fShaderTransform.preScale(1 / scale.width(), 1 / scale.height());
|
||||
key.fBitmapKey = SkBitmapKey{image.getSubset(), image.getGenerationID()};
|
||||
SkASSERT (!image.isNull());
|
||||
return make_image_shader(doc, key, std::move(image));
|
||||
|
||||
SkASSERT (!bitmap.isNull());
|
||||
bitmap.setImmutable();
|
||||
sk_sp<SkImage> image = SkImage::MakeFromBitmap(bitmap);
|
||||
return make_image_shader(doc, key, image.get());
|
||||
}
|
||||
|
||||
sk_sp<SkPDFObject> SkPDFMakeShader(SkPDFDocument* doc,
|
||||
@ -311,20 +329,14 @@ sk_sp<SkPDFObject> SkPDFMakeShader(SkPDFDocument* doc,
|
||||
{SkShader::kClamp_TileMode, SkShader::kClamp_TileMode}};
|
||||
|
||||
SkASSERT(shader->asAGradient(nullptr) == SkShader::kNone_GradientType) ;
|
||||
SkImage* skimg;
|
||||
if ((skimg = shader->isAImage(&key.fShaderTransform, key.fImageTileModes))
|
||||
&& skimg->asLegacyBitmap(&image, SkImage::kRO_LegacyBitmapMode)) {
|
||||
// TODO(halcanary): delay converting to bitmap.
|
||||
key.fBitmapKey = SkBitmapKey{image.getSubset(), image.getGenerationID()};
|
||||
if (image.isNull()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (SkImage* skimg = shader->isAImage(&key.fShaderTransform, key.fImageTileModes)) {
|
||||
key.fBitmapKey = SkBitmapKeyFromImage(skimg);
|
||||
SkPDFCanon* canon = doc->canon();
|
||||
sk_sp<SkPDFObject>* shaderPtr = canon->fImageShaderMap.find(key);
|
||||
if (shaderPtr) {
|
||||
return *shaderPtr;
|
||||
}
|
||||
sk_sp<SkPDFObject> pdfShader = make_image_shader(doc, key, std::move(image));
|
||||
sk_sp<SkPDFObject> pdfShader = make_image_shader(doc, key, skimg);
|
||||
canon->fImageShaderMap.set(std::move(key), pdfShader);
|
||||
return pdfShader;
|
||||
}
|
||||
|
@ -5,15 +5,16 @@
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "SkPDFUtils.h"
|
||||
|
||||
#include "SkData.h"
|
||||
#include "SkFixed.h"
|
||||
#include "SkGeometry.h"
|
||||
#include "SkImage_Base.h"
|
||||
#include "SkPDFResourceDict.h"
|
||||
#include "SkPDFUtils.h"
|
||||
#include "SkPDFTypes.h"
|
||||
#include "SkStream.h"
|
||||
#include "SkString.h"
|
||||
#include "SkPDFTypes.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
@ -516,3 +517,16 @@ void SkPDFUtils::PopulateTilingPatternDict(SkPDFDict* pattern,
|
||||
pattern->insertObject("Matrix", SkPDFUtils::MatrixToArray(matrix));
|
||||
}
|
||||
}
|
||||
|
||||
bool SkPDFUtils::ToBitmap(const SkImage* img, SkBitmap* dst) {
|
||||
SkASSERT(img);
|
||||
SkASSERT(dst);
|
||||
SkBitmap bitmap;
|
||||
if(as_IB(img)->getROPixels(&bitmap, nullptr)) {
|
||||
SkASSERT(bitmap.dimensions() == img->dimensions());
|
||||
SkASSERT(!bitmap.drawsNothing());
|
||||
*dst = std::move(bitmap);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -4,11 +4,10 @@
|
||||
* Use of this source code is governed by a BSD-style license that can be
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
|
||||
|
||||
#ifndef SkPDFUtils_DEFINED
|
||||
#define SkPDFUtils_DEFINED
|
||||
|
||||
#include "SkPDFTypes.h"
|
||||
#include "SkPaint.h"
|
||||
#include "SkPath.h"
|
||||
#include "SkShader.h"
|
||||
@ -124,6 +123,8 @@ void PopulateTilingPatternDict(SkPDFDict* pattern,
|
||||
SkRect& bbox,
|
||||
sk_sp<SkPDFDict> resources,
|
||||
const SkMatrix& matrix);
|
||||
|
||||
bool ToBitmap(const SkImage* img, SkBitmap* dst);
|
||||
} // namespace SkPDFUtils
|
||||
|
||||
#endif
|
||||
|
Loading…
Reference in New Issue
Block a user