Index: src/codec/SkScaledCodec.cpp |
diff --git a/src/codec/SkScaledCodec.cpp b/src/codec/SkScaledCodec.cpp |
index 01e29ad86ac5ac353ff0dc7a5e64a5b3a95791a5..afd49cc097016285b25255c1f7658406bc7d9d75 100644 |
--- a/src/codec/SkScaledCodec.cpp |
+++ b/src/codec/SkScaledCodec.cpp |
@@ -18,7 +18,7 @@ SkCodec* SkScaledCodec::NewFromStream(SkStream* stream) { |
} |
if (isWebp) { |
// Webp codec supports scaling and subsetting natively |
- return SkWebpCodec::NewFromStream(stream); |
+ return SkWebpCodec::NewFromStream(stream); |
} |
SkAutoTDelete<SkCodec> codec(SkCodec::NewFromStream(stream)); |
@@ -44,20 +44,28 @@ SkScaledCodec::SkScaledCodec(SkCodec* codec) |
SkScaledCodec::~SkScaledCodec() {} |
-bool SkScaledCodec::onRewind() { |
- return fCodec->onRewind(); |
+static bool is_in_subset(int coord, int offset, int length) { |
+ if (coord < offset || coord >= offset + length) { |
+ return false; |
+ } |
+ return true; |
} |
-static SkISize best_scaled_dimensions(const SkISize& origDims, const SkISize& nativeDims, |
- const SkISize& scaledCodecDims, float desiredScale) { |
- if (nativeDims == scaledCodecDims) { |
- // does not matter which to return if equal. Return here to skip below calculations |
- return nativeDims; |
+static int get_sample_size(float scale) { |
+ return SkScalarRoundToInt(1.0f / scale); |
+} |
+ |
+static bool use_native_scaling(const SkISize& origDims, const SkISize& nativeDims, |
+ const SkISize& sampledDims, float desiredScale) { |
+ if (nativeDims == sampledDims) { |
+ // If both options are the same, choose native scaling. |
+ return true; |
} |
+ |
float idealWidth = origDims.width() * desiredScale; |
float idealHeight = origDims.height() * desiredScale; |
- // calculate difference between native dimensions and ideal dimensions |
+ // Calculate difference between native scaling and ideal scaling. |
float nativeWDiff = SkTAbs(idealWidth - nativeDims.width()); |
float nativeHDiff = SkTAbs(idealHeight - nativeDims.height()); |
float nativeDiff = nativeWDiff + nativeHDiff; |
@@ -65,52 +73,78 @@ static SkISize best_scaled_dimensions(const SkISize& origDims, const SkISize& na |
// Native scaling is preferred to sampling. If we can scale natively to |
// within one of the ideal value, we should choose to scale natively. |
if (nativeWDiff < 1.0f && nativeHDiff < 1.0f) { |
- return nativeDims; |
+ return true; |
} |
- // calculate difference between scaledCodec dimensions and ideal dimensions |
- float scaledCodecWDiff = SkTAbs(idealWidth - scaledCodecDims.width()); |
- float scaledCodecHDiff = SkTAbs(idealHeight - scaledCodecDims.height()); |
- float scaledCodecDiff = scaledCodecWDiff + scaledCodecHDiff; |
+ // Calculate difference between native scaling and sampled scaling. |
+ float sampledWDiff = SkTAbs(idealWidth - sampledDims.width()); |
+ float sampledHDiff = SkTAbs(idealHeight - sampledDims.height()); |
+ float sampledDiff = sampledWDiff + sampledHDiff; |
- // return dimensions closest to ideal dimensions. |
- // If the differences are equal, return nativeDims, as native scaling is more efficient. |
- return nativeDiff > scaledCodecDiff ? scaledCodecDims : nativeDims; |
+ // Use native scaling if it is closer to the ideal scale. |
+ return nativeDiff <= sampledDiff; |
} |
/* |
* Return a valid set of output dimensions for this decoder, given an input scale |
*/ |
SkISize SkScaledCodec::onGetScaledDimensions(float desiredScale) const { |
- SkISize nativeDimensions = fCodec->getScaledDimensions(desiredScale); |
- // support scaling down by integer numbers. Ex: 1/2, 1/3, 1/4 ... |
- SkISize scaledCodecDimensions; |
- if (desiredScale > 0.5f) { |
- // sampleSize = 1 |
- scaledCodecDimensions = fCodec->getInfo().dimensions(); |
+ SkISize nativeDims = fCodec->getScaledDimensions(desiredScale); |
+ int sampleSize = get_sample_size(desiredScale); |
+ SkISize sampledDims = SkISize::Make( |
+ get_scaled_dimension(this->getInfo().width(), sampleSize), |
+ get_scaled_dimension(this->getInfo().height(), sampleSize)); |
+ |
+ if (use_native_scaling(this->getInfo().dimensions(), nativeDims, sampledDims, desiredScale)) { |
+ return nativeDims; |
} |
- // sampleSize determines the step size between samples |
- // Ex: sampleSize = 2, sample every second pixel in x and y directions |
- int sampleSize = int ((1.0f / desiredScale) + 0.5f); |
- int scaledWidth = get_scaled_dimension(this->getInfo().width(), sampleSize); |
- int scaledHeight = get_scaled_dimension(this->getInfo().height(), sampleSize); |
+ return sampledDims; |
+} |
- // Return the calculated output dimensions for the given scale |
- scaledCodecDimensions = SkISize::Make(scaledWidth, scaledHeight); |
+bool SkScaledCodec::onGetScaledSubsetDimensions(float desiredScale, Options* options) const { |
+ |
+ SkISize nativeDims = fCodec->getScaledDimensions(desiredScale); |
+ int sampleSize = get_sample_size(desiredScale); |
+ SkISize sampledDims = SkISize::Make( |
+ get_scaled_dimension(this->getInfo().width(), sampleSize), |
+ get_scaled_dimension(this->getInfo().height(), sampleSize)); |
+ |
+ if (use_native_scaling(this->getInfo().dimensions(), nativeDims, sampledDims, desiredScale)) { |
+ // Set the scaled dimensions and calculate subset size using native scaling. |
+ options->fScaledDimensions = nativeDims; |
+ float widthScale = ((float) nativeDims.width()) / ((float) this->getInfo().width()); |
+ float heightScale = ((float) nativeDims.height()) / ((float) this->getInfo().height()); |
+ // Notice that we may round the size of the subset up to 1. This means that we must |
+ // floor the left and top offsets to ensure that we do not suggest a subset that is |
+ // off the edge of the image. |
+ options->fScaledSubset = SkIRect::MakeXYWH( |
+ int (((float) options->fSubset->left()) * widthScale), |
+ int (((float) options->fSubset->top()) * heightScale), |
+ SkTMax(1, SkScalarRoundToInt(((float) options->fSubset->width()) * widthScale)), |
+ SkTMax(1, SkScalarRoundToInt(((float) options->fSubset->height()) * heightScale))); |
+ return true; |
+ } |
- return best_scaled_dimensions(this->getInfo().dimensions(), nativeDimensions, |
- scaledCodecDimensions, desiredScale); |
+ // Set the scaled dimensions and calculate subset size using sampling. |
+ options->fScaledDimensions = sampledDims; |
+ options->fScaledSubset = SkIRect::MakeXYWH( |
+ options->fSubset->left() / sampleSize, |
+ options->fSubset->top() / sampleSize, |
+ get_scaled_dimension(options->fSubset->width(), sampleSize), |
+ get_scaled_dimension(options->fSubset->height(), sampleSize)); |
+ return true; |
} |
// check if scaling to dstInfo size from srcInfo size using sampleSize is possible |
-static bool scaling_supported(const SkISize& dstDim, const SkISize& srcDim, |
+static bool scaling_supported(const SkISize& dstSize, const SkISize& srcSize, |
int* sampleX, int* sampleY) { |
- SkScaledCodec::ComputeSampleSize(dstDim, srcDim, sampleX, sampleY); |
- const int dstWidth = dstDim.width(); |
- const int dstHeight = dstDim.height(); |
- const int srcWidth = srcDim.width(); |
- const int srcHeight = srcDim.height(); |
+ SkScaledCodec::ComputeSampleSize(dstSize, srcSize, sampleX, sampleY); |
+ const int dstWidth = dstSize.width(); |
+ const int dstHeight = dstSize.height(); |
+ const int srcWidth = srcSize.width(); |
+ const int srcHeight = srcSize.height(); |
+ |
// only support down sampling, not up sampling |
if (dstWidth > srcWidth || dstHeight > srcHeight) { |
return false; |
@@ -151,12 +185,12 @@ bool SkScaledCodec::onDimensionsSupported(const SkISize& dim) { |
} |
// calculates sampleSize in x and y direction |
-void SkScaledCodec::ComputeSampleSize(const SkISize& dstDim, const SkISize& srcDim, |
+void SkScaledCodec::ComputeSampleSize(const SkISize& dstSize, const SkISize& srcSize, |
int* sampleXPtr, int* sampleYPtr) { |
- int srcWidth = srcDim.width(); |
- int dstWidth = dstDim.width(); |
- int srcHeight = srcDim.height(); |
- int dstHeight = dstDim.height(); |
+ int srcWidth = srcSize.width(); |
+ int dstWidth = dstSize.width(); |
+ int srcHeight = srcSize.height(); |
+ int dstHeight = dstSize.height(); |
int sampleX = srcWidth / dstWidth; |
int sampleY = srcHeight / dstHeight; |
@@ -183,6 +217,7 @@ void SkScaledCodec::ComputeSampleSize(const SkISize& dstDim, const SkISize& srcD |
} else if (get_scaled_dimension(srcHeight, sampleX) == dstHeight) { |
sampleY = sampleX; |
} |
+ // FIXME (msarett): Should this never be reached? |
} |
} |
@@ -194,66 +229,173 @@ void SkScaledCodec::ComputeSampleSize(const SkISize& dstDim, const SkISize& srcD |
} |
} |
-// TODO: Implement subsetting in onGetPixels which works when and when not sampling |
+SkCodec::Result SkScaledCodec::onGetPixels(const SkImageInfo& scaledSubsetInfo, void* dst, |
+ size_t rowBytes, const Options& options, SkPMColor ctable[], int* ctableCount, |
+ int* rowsDecoded) { |
+ |
+ // There are various values of Rect, Size, and Info used in this function. I think it is |
+ // useful to go ahead and define what they mean. |
+ // orig_: Refers to the original image size. |
+ // scaledSubset_: Refers to the size of the final output. This can match the original |
+ // dimensions, be a subset of the original dimensions, be a scaled version |
+ // of the original dimensions, or be a scaled subset of the original dimensions. |
+ // subset_: Refers to the size of the unscaled subset in terms of the original image |
+ // dimensions. If this is not a subset decode, this will match the original |
+ // image dimensions. |
+ // scaled_: Refers to the scaled size of the original image, ignoring any subsetting. |
+ // If we are not scaling, this will match the original dimensions. |
+ SkISize origSize = this->getInfo().dimensions(); |
+ SkIRect subsetRect; |
+ SkISize scaledSize; |
+ SkIRect scaledSubsetRect; |
+ if (nullptr == options.fSubset) { |
+ // This is not a subset decode. |
+ SkASSERT(options.fScaledDimensions.isZero()); |
+ SkASSERT(options.fScaledSubset.isEmpty()); |
+ |
+ // Set the "subset" to the full image dimensions. |
+ subsetRect = SkIRect::MakeSize(origSize); |
+ |
+ // This may be scaled or unscaled, depending on if scaledSize matches origSize. |
+ scaledSize = scaledSubsetInfo.dimensions(); |
+ scaledSubsetRect = SkIRect::MakeSize(scaledSize); |
+ } else { |
+ // This is a subset decode. |
+ if (!is_valid_subset(options.fSubset, origSize)) { |
+ return kInvalidParameters; |
+ } |
-SkCodec::Result SkScaledCodec::onGetPixels(const SkImageInfo& requestedInfo, void* dst, |
- size_t rowBytes, const Options& options, |
- SkPMColor ctable[], int* ctableCount, |
- int* rowsDecoded) { |
+ subsetRect = *(options.fSubset); |
+ if (options.fScaledDimensions.isZero()) { |
+ // This is an unscaled subset decode. |
+ SkASSERT(options.fScaledSubset.isEmpty()); |
+ SkASSERT(scaledSubsetInfo.dimensions() == subsetRect.size()); |
+ |
+ scaledSize = origSize; |
+ scaledSubsetRect = subsetRect; |
+ } else { |
+ // This is a scaled subset decode. |
+ SkASSERT(!options.fScaledSubset.isEmpty()); |
+ SkASSERT(scaledSubsetInfo.dimensions() == options.fScaledSubset.size()); |
+ if (!is_valid_subset(&options.fScaledSubset, options.fScaledDimensions)) { |
+ return kInvalidParameters; |
+ } |
- if (options.fSubset) { |
- // Subsets are not supported. |
- return kUnimplemented; |
+ scaledSize = options.fScaledDimensions; |
+ scaledSubsetRect = options.fScaledSubset; |
+ } |
} |
- if (fCodec->dimensionsSupported(requestedInfo.dimensions())) { |
- // Make sure that the parent class does not fill on an incomplete decode, since |
- // fCodec will take care of filling the uninitialized lines. |
- *rowsDecoded = requestedInfo.height(); |
- return fCodec->getPixels(requestedInfo, dst, rowBytes, &options, ctable, ctableCount); |
+ // Reset the options for use by fCodec. We will handle scaling and subsetting |
+ // from this level, the native codec does not need to know about it. |
+ Options newOptions = options; |
+ newOptions.fSubset = nullptr; |
+ newOptions.fScaledDimensions = SkISize::Make(0, 0); |
+ newOptions.fScaledSubset = SkIRect::MakeEmpty(); |
+ |
+ // The native decoder needs the scaled size of the entire image to check if it can decode to |
+ // the requested scale. |
+ SkImageInfo scaledInfo = scaledSubsetInfo.makeWH(scaledSize.width(), scaledSize.height()); |
+ Result result = fCodec->startScanlineDecode(scaledInfo, &newOptions, ctable, ctableCount, |
+ scaledSubsetRect.left(), scaledSubsetRect.width()); |
+ switch (result) { |
+ case kSuccess: |
+ return this->nativeDecode(scaledInfo, dst, rowBytes, scaledSubsetRect, |
+ options.fZeroInitialized, rowsDecoded); |
+ case kInvalidScale: |
+ // We will attempt to scale by sampling below. |
+ break; |
+ default: |
+ return result; |
} |
- // scaling requested |
+ // Try to provide the scale using sampling. |
int sampleX; |
int sampleY; |
- if (!scaling_supported(requestedInfo.dimensions(), fCodec->getInfo().dimensions(), |
- &sampleX, &sampleY)) { |
- // onDimensionsSupported would have returned false, meaning we should never reach here. |
- SkASSERT(false); |
+ if (!scaling_supported(scaledSubsetInfo.dimensions(), subsetRect.size(), &sampleX, &sampleY)) { |
return kInvalidScale; |
} |
- // set first sample pixel in y direction |
- const int Y0 = get_start_coord(sampleY); |
- |
- const int dstHeight = requestedInfo.height(); |
- const int srcWidth = fCodec->getInfo().width(); |
- const int srcHeight = fCodec->getInfo().height(); |
- |
- const SkImageInfo info = requestedInfo.makeWH(srcWidth, srcHeight); |
- |
- Result result = fCodec->startScanlineDecode(info, &options, ctable, ctableCount); |
- |
+ // Create the image info that will be passed to fCodec. We support scaling in the |
+ // x-dimension, but we will perform scaling in the y-dimension here, so we pass the scaled |
+ // width and the original height. |
+ // FIXME: This is a bit confusing. I think there is a plan to move scaling completely out |
+ // of the native codec. |
+ SkImageInfo decodeInfo = scaledSubsetInfo.makeWH(scaledSize.width(), this->getInfo().height()); |
+ |
+ // When starting the fCodec, we pass the left offset based on the original image |
+ // dimensions, but need the scaled version of the subset width. |
+ // FIXME: This is a bit confusing. I think there is a plan to move scaling completely out |
+ // of the native codec. |
+ result = fCodec->startScanlineDecode(decodeInfo, &newOptions, ctable, ctableCount, |
+ subsetRect.left(), scaledSubsetInfo.width()); |
if (kSuccess != result) { |
+ SkASSERT(kInvalidScale != result); |
return result; |
} |
- SkSampler* sampler = fCodec->getSampler(true); |
- if (!sampler) { |
- return kUnimplemented; |
- } |
+ return this->sampledDecode(scaledSubsetInfo, dst, rowBytes, subsetRect, scaledSubsetRect, |
+ sampleX, sampleY, options.fZeroInitialized, rowsDecoded); |
+} |
- if (sampler->setSampleX(sampleX) != requestedInfo.width()) { |
- return kInvalidScale; |
+SkCodec::Result SkScaledCodec::nativeDecode(const SkImageInfo& scaledInfo, void* dst, |
+ size_t rowBytes, const SkIRect& scaledSubsetRect, ZeroInitialized zeroInit, |
+ int* rowsDecoded) { |
+ |
+ int scaledSubsetTop = scaledSubsetRect.top(); |
+ int scaledSubsetHeight = scaledSubsetRect.height(); |
+ switch (fCodec->getScanlineOrder()) { |
+ case SkCodec::kTopDown_SkScanlineOrder: |
+ case SkCodec::kBottomUp_SkScanlineOrder: |
+ case SkCodec::kNone_SkScanlineOrder: { |
+ if (!fCodec->skipScanlines(scaledSubsetTop)) { |
+ *rowsDecoded = 0; |
+ return kIncompleteInput; |
+ } |
+ |
+ uint32_t decodedLines = fCodec->getScanlines(dst, scaledSubsetHeight, |
+ rowBytes); |
+ if (decodedLines != scaledSubsetHeight) { |
+ *rowsDecoded = decodedLines; |
+ return kIncompleteInput; |
+ } |
+ return kSuccess; |
+ } |
+ case SkCodec::kOutOfOrder_SkScanlineOrder: { |
+ for (int y = 0; y < scaledInfo.height(); y++) { |
+ int dstY = fCodec->nextScanline(); |
+ if (is_in_subset(dstY, scaledSubsetTop, scaledSubsetHeight)) { |
+ void* dstPtr = SkTAddOffset<void>(dst, rowBytes * (dstY - scaledSubsetTop)); |
+ if (1 != fCodec->getScanlines(dstPtr, 1, rowBytes)) { |
+ *rowsDecoded = y + 1; |
+ return kIncompleteInput; |
+ } |
+ } else { |
+ if (!fCodec->skipScanlines(1)) { |
+ *rowsDecoded = y + 1; |
+ return kIncompleteInput; |
+ } |
+ } |
+ } |
+ return kSuccess; |
+ } |
} |
+} |
+ |
+SkCodec::Result SkScaledCodec::sampledDecode(const SkImageInfo& scaledSubsetInfo, void* dst, |
+ size_t rowBytes, const SkIRect& subsetRect, const SkIRect& scaledSubsetRect, int sampleX, |
+ int sampleY, ZeroInitialized zeroInit, int* rowsDecoded) { |
+ // Set first sample pixel in y direction. |
+ int y0 = get_start_coord(sampleY); |
+ int scaledSubsetHeight = scaledSubsetRect.height(); |
switch(fCodec->getScanlineOrder()) { |
- case SkCodec::kTopDown_SkScanlineOrder: { |
- if (!fCodec->skipScanlines(Y0)) { |
+ case SkCodec::kTopDown_SkScanlineOrder: |
+ if (!fCodec->skipScanlines(y0 + subsetRect.top())) { |
*rowsDecoded = 0; |
return kIncompleteInput; |
} |
- for (int y = 0; y < dstHeight; y++) { |
+ for (int y = 0; y < scaledSubsetHeight; y++) { |
if (1 != fCodec->getScanlines(dst, 1, rowBytes)) { |
// The failed call to getScanlines() will take care of |
// filling the failed row, so we indicate that we have |
@@ -261,7 +403,7 @@ SkCodec::Result SkScaledCodec::onGetPixels(const SkImageInfo& requestedInfo, voi |
*rowsDecoded = y + 1; |
return kIncompleteInput; |
} |
- if (y < dstHeight - 1) { |
+ if (y < scaledSubsetHeight - 1) { |
if (!fCodec->skipScanlines(sampleY - 1)) { |
*rowsDecoded = y + 1; |
return kIncompleteInput; |
@@ -270,14 +412,13 @@ SkCodec::Result SkScaledCodec::onGetPixels(const SkImageInfo& requestedInfo, voi |
dst = SkTAddOffset<void>(dst, rowBytes); |
} |
return kSuccess; |
- } |
case SkCodec::kBottomUp_SkScanlineOrder: |
case SkCodec::kOutOfOrder_SkScanlineOrder: { |
Result result = kSuccess; |
int y; |
- for (y = 0; y < srcHeight; y++) { |
+ for (y = 0; y < this->getInfo().height(); y++) { |
int srcY = fCodec->nextScanline(); |
- if (is_coord_necessary(srcY, sampleY, dstHeight)) { |
+ if (is_coord_necessary(srcY, sampleY, scaledSubsetHeight, subsetRect.top())) { |
void* dstPtr = SkTAddOffset<void>(dst, rowBytes * get_dst_coord(srcY, sampleY)); |
if (1 != fCodec->getScanlines(dstPtr, 1, rowBytes)) { |
result = kIncompleteInput; |
@@ -294,38 +435,43 @@ SkCodec::Result SkScaledCodec::onGetPixels(const SkImageInfo& requestedInfo, voi |
// We handle filling uninitialized memory here instead of in the parent class. |
// The parent class does not know that we are sampling. |
if (kIncompleteInput == result) { |
- const uint32_t fillValue = fCodec->getFillValue(requestedInfo.colorType(), |
- requestedInfo.alphaType()); |
- for (; y < srcHeight; y++) { |
+ const uint32_t fillValue = fCodec->getFillValue(scaledSubsetInfo.colorType(), |
+ scaledSubsetInfo.alphaType()); |
+ for (; y < this->getInfo().height(); y++) { |
int srcY = fCodec->outputScanline(y); |
- if (is_coord_necessary(srcY, sampleY, dstHeight)) { |
+ if (is_coord_necessary(srcY, sampleY, scaledSubsetHeight, subsetRect.top())) { |
void* dstRow = SkTAddOffset<void>(dst, |
rowBytes * get_dst_coord(srcY, sampleY)); |
- SkSampler::Fill(dstRow, requestedInfo.colorType(), requestedInfo.width(), |
- 1, rowBytes, fillValue, options.fZeroInitialized); |
+ SkSampler::Fill(dstRow, scaledSubsetInfo.colorType(), |
+ scaledSubsetInfo.width(), 1, rowBytes, fillValue, |
+ zeroInit); |
} |
} |
- *rowsDecoded = dstHeight; |
+ *rowsDecoded = scaledSubsetInfo.height(); |
} |
return result; |
} |
case SkCodec::kNone_SkScanlineOrder: { |
- SkAutoMalloc storage(srcHeight * rowBytes); |
+ SkAutoMalloc storage(subsetRect.height() * rowBytes); |
uint8_t* storagePtr = static_cast<uint8_t*>(storage.get()); |
- int scanlines = fCodec->getScanlines(storagePtr, srcHeight, rowBytes); |
- storagePtr += Y0 * rowBytes; |
- scanlines -= Y0; |
+ if (!fCodec->skipScanlines(subsetRect.top())) { |
+ *rowsDecoded = 0; |
+ return kIncompleteInput; |
+ } |
+ int scanlines = fCodec->getScanlines(storagePtr, subsetRect.height(), rowBytes); |
+ scanlines -= y0; |
+ storagePtr += y0 * rowBytes; |
int y = 0; |
- while (y < dstHeight && scanlines > 0) { |
- memcpy(dst, storagePtr, rowBytes); |
+ while (y < scaledSubsetHeight && scanlines > 0) { |
+ memcpy(dst, storagePtr, scaledSubsetInfo.minRowBytes()); |
storagePtr += sampleY * rowBytes; |
dst = SkTAddOffset<void>(dst, rowBytes); |
scanlines -= sampleY; |
y++; |
} |
- if (y < dstHeight) { |
+ if (y < scaledSubsetHeight) { |
// fCodec has already handled filling uninitialized memory. |
- *rowsDecoded = dstHeight; |
+ *rowsDecoded = scaledSubsetInfo.height(); |
return kIncompleteInput; |
} |
return kSuccess; |
@@ -336,6 +482,10 @@ SkCodec::Result SkScaledCodec::onGetPixels(const SkImageInfo& requestedInfo, voi |
} |
} |
+bool SkScaledCodec::onRewind() { |
+ return fCodec->onRewind(); |
+} |
+ |
uint32_t SkScaledCodec::onGetFillValue(SkColorType colorType, SkAlphaType alphaType) const { |
return fCodec->onGetFillValue(colorType, alphaType); |
} |