Skip to content

Commit ca81ecb

Browse files
matmenclaude
andcommitted
WebP: sharpYuv
encodeWebP({ quality, sharpYuv: true }) (images, animations, Animation.webpEncoder) converts to YUV with libwebp's sharp conversion, ported in src/codecs/sharpyuv.ts (sharpyuv.c, sharpyuv_dsp.c, sharpyuv_gamma.c for 8 bit RGB and YUV, the WebP matrix): 4 iterations choosing luma and chroma so the upsampled picture matches the original, from 4x4 pixels on like libwebp; alpha and the transparent area cleanup stay as before. The VP8 chunks equal cwebp -sharp_yuv's: 3 images x methods 4 and 2 in tests/webp-lossy.js (scripts/webp-lossy-references.py makes them), and odd sizes, 4x4, 3x5, a graphic and an image with alpha checked by hand. 1920x1080 photo at quality 75: 101924 instead of 101624 bytes, 581 vs 302 ms; 1200x800 red/blue squares: 2.7x the bytes (libwebp's too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
1 parent 538b5d2 commit ca81ecb

10 files changed

Lines changed: 235 additions & 26 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
159 MB instead of 1066 MB as an `Animation`. Unoptimized WebPs with frames WebP can't store as they are (disposed to
1515
the previous frame, or disposed at odd offsets) store the canvas from the first of them on (`encodeWebP()` from the
1616
start), which looks the same.
17+
- `encodeWebP({ quality, sharpYuv: true })` (images, animations and `Animation.webpEncoder`) converts to YUV like
18+
libwebp's sharp conversion (`cwebp -sharp_yuv`, ported: the same files), which keeps colored edges sharp. It takes
19+
about twice the time at the default effort; photos get about as large (+0.3% on a 1920x1080 photo), images with
20+
many hard color edges a lot larger.
1721
- `Image.renderText()` takes OpenType features: `features: { tnum: true, smcp: true, liga: false, salt: 2 }` turns
1822
features on for the whole text (tabular numbers, small caps, stylistic sets, ...), off (also the default ones like
1923
ligatures or kerning) or picks an alternate by its number, like HarfBuzz's user features (shaped exactly like

‎THIRD_PARTY_NOTICES.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ following projects, under their own licenses. Each derived file says so in its h
1212
| [font-rs](https://github.com/raphlinus/font-rs) by Raph Levien | `src/render/text.ts` (the `Rasterizer` class, also used for SVGs) | Apache-2.0 |
1313
| [RFC 9649](https://www.rfc-editor.org/rfc/rfc9649) (WebP Image Format) | `src/codecs/vp8l.ts` (lossless bitstream, short distance code table), `src/codecs/webp.ts` (container) | BSD-3-Clause (Revised BSD) |
1414
| [RFC 6386](https://www.rfc-editor.org/rfc/rfc6386) (VP8 Data Format and Decoding Guide, with the libvpx reference decoder) | `src/codecs/vp8-tables.ts` (probability and quantizer tables), `src/codecs/vp8.ts` (decoding procedures) | BSD-3-Clause |
15-
| [libwebp](https://chromium.googlesource.com/webm/libwebp) | `src/codecs/vp8.ts` (YUV to RGB conversion, "fancy" chroma upsampling), `src/codecs/webp.ts` (animation frame blending), `src/codecs/vp8-enc.ts`, `src/codecs/vp8-enc-dsp.ts` (the lossy encoder: RGB to YUV conversion, analysis, segments, rate-distortion optimized mode decisions, quantization, token probabilities, bitstream, alpha filtering), `src/codecs/vp8-enc-tables.ts` (its tables, generated from libwebp by `scripts/vp8-enc-tables.py`) | BSD-3-Clause |
15+
| [libwebp](https://chromium.googlesource.com/webm/libwebp) | `src/codecs/vp8.ts` (YUV to RGB conversion, "fancy" chroma upsampling), `src/codecs/webp.ts` (animation frame blending), `src/codecs/vp8-enc.ts`, `src/codecs/vp8-enc-dsp.ts` (the lossy encoder: RGB to YUV conversion, analysis, segments, rate-distortion optimized mode decisions, quantization, token probabilities, bitstream, alpha filtering), `src/codecs/vp8-enc-tables.ts` (its tables, generated from libwebp by `scripts/vp8-enc-tables.py`), `src/codecs/sharpyuv.ts` (the sharp RGB to YUV conversion, libsharpyuv) | BSD-3-Clause |
1616
| [dav1d](https://code.videolan.org/videolan/dav1d) by VideoLAN, the dav1d authors and Two Orioles | `src/codecs/av1/*` (the AV1 decoder, and the encoder's syntax, contexts and reconstruction mirrored from it: bitstream parsing, symbol decoding, prediction, inverse transforms, loop filters, film grain; the tables in `src/codecs/av1/data.ts`, generated from dav1d by `scripts/av1-tables.c`) | BSD-2-Clause |
1717
| [libaom](https://aomedia.googlesource.com/aom/) by the Alliance for Open Media | `src/codecs/av1/msac-enc.ts` (the symbol encoder), `src/codecs/av1/fdct.ts` (the forward Walsh-Hadamard transform), `src/codecs/av1/encode.ts` (the loop filter level from the quantizer), `src/codecs/avif-enc.ts` (the quantizer to qindex table) | BSD-2-Clause |
1818
| [libavif](https://github.com/AOMediaCodec/libavif) by Joe Drago | `src/codecs/avif.ts` (YUV to RGB conversion with bilinear chroma upsampling, alpha range expansion and unpremultiplication) | BSD-2-Clause |

‎scripts/webp-lossy-references.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Prints the sha256 of the VP8 chunks cwebp (libwebp) writes for some of tests/targets, with its default settings and
2-
# a quality (and some with other methods, ImageScript's efforts), for tests/webp-lossy.js (ImageScript's lossy encoder writes the same). The images are passed as PPM/PAM,
2+
# a quality (and some with other methods, ImageScript's efforts, or -sharp_yuv), for tests/webp-lossy.js (ImageScript's lossy encoder writes the same). The images are passed as PPM/PAM,
33
# which cwebp reads without libpng:
44
# CWEBP=path/to/cwebp python scripts/webp-lossy-references.py
55
import hashlib, os, struct, subprocess, tempfile
@@ -14,6 +14,8 @@
1414
# the other methods (at quality 75)
1515
method_images = ['external.png', 'potato.png', 'font-4.png']
1616
methods = [0, 1, 2, 3, 5, 6]
17+
# -sharp_yuv (at quality 75, methods 4 and 2)
18+
sharp_images = ['external.png', 'potato.png', 'font-4.png']
1719

1820

1921
def vp8_chunk(data):
@@ -33,8 +35,9 @@ def vp8_chunk(data):
3335
src = tmp / ('in.pam' if alpha else 'in.ppm')
3436
header = f'P7\nWIDTH {w}\nHEIGHT {h}\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n' if alpha else f'P6\n{w} {h}\n255\n'
3537
src.write_bytes(header.encode() + im.tobytes())
36-
runs = [(q, 4) for q in qualities] + [(75, m) for m in methods if name in method_images]
37-
for q, m in runs:
38-
subprocess.run([CWEBP, '-quiet', '-q', str(q), '-m', str(m), str(src), '-o', str(tmp / 'out.webp')], check=True)
39-
key = f'{name} {q}' if m == 4 else f'{name} {q} {m}'
38+
runs = [(q, 4, False) for q in qualities] + [(75, m, False) for m in methods if name in method_images]
39+
runs += [(75, m, True) for m in [4, 2] if name in sharp_images]
40+
for q, m, sharp in runs:
41+
subprocess.run([CWEBP, '-quiet', '-q', str(q), '-m', str(m), *(['-sharp_yuv'] if sharp else []), str(src), '-o', str(tmp / 'out.webp')], check=True)
42+
key = f'{name} {q} {m} sharp' if sharp else f'{name} {q}' if m == 4 else f'{name} {q} {m}'
4043
print(f" '{key}': '{hashlib.sha256(vp8_chunk((tmp / 'out.webp').read_bytes())).hexdigest()}',")

‎src/animation-encoder.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export interface WebPAnimationEncoderOptions extends AnimationEncoderOptions {
3030
quality?: number;
3131
/** How much effort lossy frames take (0-6), see {@link WebPEncodeOptions.effort}. Default: `2` */
3232
effort?: number;
33+
/** Lossy frames: libwebp's sharp YUV conversion, see {@link WebPEncodeOptions.sharpYuv}. Default: `false` */
34+
sharpYuv?: boolean;
3335
}
3436

3537
/** Options of {@link Animation.gifEncoder} */
@@ -118,7 +120,7 @@ export class AnimationEncoder {
118120
}
119121

120122
/** @internal */
121-
static __webp__(width: number, height: number, { loopCount = -1, metadata = {}, optimize = true, quality, effort = 2 }: WebPAnimationEncoderOptions): AnimationEncoder {
123+
static __webp__(width: number, height: number, { loopCount = -1, metadata = {}, optimize = true, quality, effort = 2, sharpYuv = false }: WebPAnimationEncoderOptions): AnimationEncoder {
122124
canvas(width, height, loopCount);
123125
if (width > 1 << 24 || height > 1 << 24) throw new RangeError('WebP: canvas too large');
124126
if (quality !== undefined) {
@@ -134,7 +136,7 @@ export class AnimationEncoder {
134136
let full = false;
135137
const add = (f: webp.WebPEncodeFrame): void => {
136138
alpha ||= webp.needsAlpha(f, !chunks.length, width, height);
137-
chunks.push(webp.frameChunk(f, quality, effort));
139+
chunks.push(webp.frameChunk(f, quality, effort, sharpYuv));
138140
};
139141
return new AnimationEncoder(width, height, {
140142
optimize: { replace: true, even: true, alpha: 'frame', tolerance: quality === undefined ? undefined : webp.lossyTolerance(quality) },

‎src/animation.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export interface WebPAnimationEncodeOptions extends AnimationEncodeOptions {
6666
quality?: number;
6767
/** How much effort lossy frames take (0-6), see {@link WebPEncodeOptions.effort}. Default: `2` */
6868
effort?: number;
69+
/** Lossy frames: libwebp's sharp YUV conversion, see {@link WebPEncodeOptions.sharpYuv}. Default: `false` */
70+
sharpYuv?: boolean;
6971
}
7072

7173
/** Options of {@link Animation.encodeGIF} */
@@ -289,7 +291,7 @@ export class Animation extends Array<Frame> {
289291
* shows the same animation, but doesn't necessarily decode to the same frames
290292
* @param options See {@link WebPAnimationEncodeOptions}
291293
*/
292-
async encodeWebP({ metadata = {}, optimize = true, quality, effort = 2 }: WebPAnimationEncodeOptions = {}): Promise<Uint8Array> {
294+
async encodeWebP({ metadata = {}, optimize = true, quality, effort = 2, sharpYuv = false }: WebPAnimationEncodeOptions = {}): Promise<Uint8Array> {
293295
this.__validate__();
294296
if (!this.length) throw new Error('An animation needs at least one frame');
295297
if (quality !== undefined) {
@@ -300,7 +302,7 @@ export class Animation extends Array<Frame> {
300302

301303
const tolerance = quality === undefined ? undefined : webp.lossyTolerance(quality);
302304
const frames = optimize ? this.__optimized__({ replace: true, even: true, alpha: 'frame', tolerance }) : this.__webpFrames__();
303-
return webp.encodeAnimation(frames, this.width, this.height, toPlays(this.loopCount), exifData(resolveMetadata(metadata)), quality, effort);
305+
return webp.encodeAnimation(frames, this.width, this.height, toPlays(this.loopCount), exifData(resolveMetadata(metadata)), quality, effort, sharpYuv);
304306
}
305307

306308
/**

‎src/codecs/sharpyuv.ts‎

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*!
2+
* libwebp's sharp RGB to YUV 4:2:0 conversion (sharpyuv/sharpyuv.c, sharpyuv_dsp.c, sharpyuv_gamma.c), ported for 8
3+
* bit RGB to 8 bit YUV with the WebP matrix: luma and chroma are chosen iteratively so that the picture upsampled
4+
* again matches the original better, sharper at color edges than averaging the chroma. Copyright (c) 2022 Google
5+
* Inc., BSD-3-Clause License (see THIRD_PARTY_NOTICES.md)
6+
*/
7+
8+
const ITERATIONS = 4;
9+
// RGB is 8 bits, the work is done in 10 (2 more bits of precision, sharpyuv's GetPrecisionShift)
10+
const SHIFT = 2, BITS = 10, MAX = (1 << BITS) - 1;
11+
12+
// the gamma curve (libwebp calls it sRGB: Rec. 709's), as 16 bit linear values from 10 bit ones and back by a 513
13+
// entry table
14+
const TO_LINEAR = new Uint32Array(1024 + 2), FROM_LINEAR = new Uint32Array(512 + 2);
15+
{
16+
const a = 0.09929682680944, thresh = 0.018053968510807, gamma = 1 / 0.45;
17+
for (let v = 0; v <= 1024; v++) {
18+
const g = v / 1024;
19+
TO_LINEAR[v] = Math.trunc((g <= thresh * 4.5 ? g / 4.5 : Math.pow((g + a) / (1 + a), gamma)) * 65536 + 0.5);
20+
}
21+
TO_LINEAR[1025] = TO_LINEAR[1024];
22+
for (let v = 0; v <= 512; v++) {
23+
const g = v / 512;
24+
FROM_LINEAR[v] = Math.trunc(65536 * (g <= thresh ? 4.5 * g : (1 + a) * Math.pow(g, 1 / gamma) - a) + 0.5);
25+
}
26+
FROM_LINEAR[513] = FROM_LINEAR[512];
27+
}
28+
29+
const toLinear = (v: number): number => TO_LINEAR[v];
30+
31+
function fromLinear(value: number): number {
32+
const pos = value >>> 7, x = value - (pos << 7);
33+
const v0 = FROM_LINEAR[pos] >>> 6, v1 = FROM_LINEAR[pos + 1] >>> 6;
34+
return v0 + (((v1 - v0) * x + 64) >>> 7);
35+
}
36+
37+
// (the values can take more than 32 bits)
38+
const gray = (r: number, g: number, b: number): number => Math.floor((13933 * r + 46871 * g + 4732 * b + 32768) / 65536);
39+
const clip = (v: number): number => v < 0 ? 0 : v > MAX ? MAX : v;
40+
41+
// the gamma compressed luma of a row of R, G and B planes (w each)
42+
function updateW(src: Uint16Array, s: number, dst: Uint16Array, d: number, w: number): void {
43+
for (let i = 0; i < w; i++) dst[d + i] = fromLinear(gray(toLinear(src[s + i]), toLinear(src[s + w + i]), toLinear(src[s + 2 * w + i])));
44+
}
45+
46+
// the average of 4 gamma compressed values, in linear light
47+
const scaleDown = (a: number, b: number, c: number, d: number): number => fromLinear((toLinear(a) + toLinear(b) + toLinear(c) + toLinear(d) + 2) >> 2);
48+
49+
// the chroma (R, G and B minus their luma) of 2 rows of R, G and B planes (2 uvWidth each) into 3 planes of uvWidth
50+
function updateChroma(src: Uint16Array, s1: number, s2: number, dst: Int16Array, d: number, uvWidth: number): void {
51+
const w = 2 * uvWidth;
52+
for (let i = 0; i < uvWidth; i++) {
53+
const o = 2 * i;
54+
const r = scaleDown(src[s1 + o], src[s1 + o + 1], src[s2 + o], src[s2 + o + 1]);
55+
const g = scaleDown(src[s1 + w + o], src[s1 + w + o + 1], src[s2 + w + o], src[s2 + w + o + 1]);
56+
const b = scaleDown(src[s1 + 2 * w + o], src[s1 + 2 * w + o + 1], src[s2 + 2 * w + o], src[s2 + 2 * w + o + 1]);
57+
const W = gray(r, g, b);
58+
dst[d + i] = r - W;
59+
dst[d + uvWidth + i] = g - W;
60+
dst[d + 2 * uvWidth + i] = b - W;
61+
}
62+
}
63+
64+
function storeGray(rgb: Uint16Array, s: number, y: Uint16Array, d: number, w: number): void {
65+
for (let i = 0; i < w; i++) y[d + i] = gray(rgb[s + i], rgb[s + w + i], rgb[s + 2 * w + i]);
66+
}
67+
68+
// a row of RGBA as R, G and B planes (w each, w even: an odd width's last pixel again), 10 bit
69+
function importRow(rgba: Uint8Array, o: number, width: number, w: number, dst: Uint16Array, d: number): void {
70+
for (let i = 0; i < width; i++, o += 4) {
71+
dst[d + i] = rgba[o] << SHIFT;
72+
dst[d + w + i] = rgba[o + 1] << SHIFT;
73+
dst[d + 2 * w + i] = rgba[o + 2] << SHIFT;
74+
}
75+
if (width & 1) for (let c = 0; c < 3; c++) dst[d + c * w + width] = dst[d + c * w + width - 1];
76+
}
77+
78+
// the RGB of 2 rows (into out at o1 and o2) from their luma and the chroma upsampled (cur, the rows' chroma, with
79+
// prev and next, the rows above and below): sharpyuv's InterpolateTwoRows
80+
function interpolateTwoRows(bestY: Uint16Array, y: number, uv: Int16Array, prev: number, cur: number, next: number, w: number, out: Uint16Array, o1: number, o2: number): void {
81+
const uvWidth = w >> 1, len = (w - 1) >> 1;
82+
const filter2 = (a: number, b: number, w0: number): number => clip(((a * 3 + b + 2) >> 2) + w0);
83+
const filterRow = (a: number, b: number, by: number, o: number): void => {
84+
for (let i = 0; i < len; i++, a++, b++) {
85+
const v0 = (uv[a] * 9 + uv[a + 1] * 3 + uv[b] * 3 + uv[b + 1] + 8) >> 4;
86+
const v1 = (uv[a + 1] * 9 + uv[a] * 3 + uv[b + 1] * 3 + uv[b] + 8) >> 4;
87+
out[o + 2 * i] = clip(bestY[by + 2 * i] + v0);
88+
out[o + 2 * i + 1] = clip(bestY[by + 2 * i + 1] + v1);
89+
}
90+
};
91+
for (let k = 0; k < 3; k++) {
92+
out[o1] = filter2(uv[cur], uv[prev], bestY[y]);
93+
out[o2] = filter2(uv[cur], uv[next], bestY[y + w]);
94+
filterRow(cur, prev, y + 1, o1 + 1);
95+
filterRow(cur, next, y + w + 1, o2 + 1);
96+
if (!(w & 1)) {
97+
out[o1 + w - 1] = filter2(uv[cur + uvWidth - 1], uv[prev + uvWidth - 1], bestY[y + w - 1]);
98+
out[o2 + w - 1] = filter2(uv[cur + uvWidth - 1], uv[next + uvWidth - 1], bestY[y + 2 * w - 1]);
99+
}
100+
o1 += w;
101+
o2 += w;
102+
prev += uvWidth;
103+
cur += uvWidth;
104+
next += uvWidth;
105+
}
106+
}
107+
108+
// the WebP matrix (limited range Rec. 601) for 10 bit RGB: Y, U and V of R, G and B, rounded
109+
const yuv = (r: number, g: number, b: number, c0: number, c1: number, c2: number, offset: number): number =>
110+
(c0 * r + c1 * g + c2 * b + (offset << 18) + (1 << 17)) >> 18;
111+
const clip8 = (v: number): number => v < 0 ? 0 : v > 255 ? 255 : v;
112+
113+
/**
114+
* The luma and chroma of RGBA pixels (their alpha isn't used) by libwebp's sharp conversion (at least 4x4 pixels, as
115+
* libwebp uses it): y has width x height values, u and v (width + 1) / 2 x (height + 1) / 2
116+
*/
117+
export function sharpYUV(rgba: Uint8Array, width: number, height: number, y: Uint8Array, u: Uint8Array, v: Uint8Array): void {
118+
const w = (width + 1) & ~1, h = (height + 1) & ~1, uvWidth = w >> 1, uvHeight = h >> 1;
119+
const tmp = new Uint16Array(w * 6), bestY = new Uint16Array(w * h), targetY = new Uint16Array(w * h), bestRGBY = new Uint16Array(w * 2);
120+
const bestUV = new Int16Array(uvWidth * 3 * uvHeight), targetUV = new Int16Array(uvWidth * 3 * uvHeight), bestRGBUV = new Int16Array(uvWidth * 3);
121+
const stride = width * 4;
122+
123+
// the start: luma of the original, chroma averaged in linear light
124+
for (let j = 0, uvRow = 0; j < height; j += 2, uvRow += 3 * uvWidth) {
125+
importRow(rgba, j * stride, width, w, tmp, 0);
126+
if (j < height - 1) importRow(rgba, (j + 1) * stride, width, w, tmp, 3 * w);
127+
else tmp.copyWithin(3 * w, 0, 3 * w);
128+
storeGray(tmp, 0, bestY, j * w, w);
129+
storeGray(tmp, 3 * w, bestY, (j + 1) * w, w);
130+
updateW(tmp, 0, targetY, j * w, w);
131+
updateW(tmp, 3 * w, targetY, (j + 1) * w, w);
132+
updateChroma(tmp, 0, 3 * w, targetUV, uvRow, uvWidth);
133+
bestUV.set(targetUV.subarray(uvRow, uvRow + 3 * uvWidth), uvRow);
134+
}
135+
136+
// the luma and chroma move by how much the upsampled picture's differ from the target's
137+
const threshold = Math.trunc(3 * w * h);
138+
let previousSum = Infinity;
139+
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
140+
let cur = 0, prev = 0, sum = 0;
141+
for (let j = 0; j < h; j += 2) {
142+
const next = cur + (j < h - 2 ? 3 * uvWidth : 0);
143+
interpolateTwoRows(bestY, j * w, bestUV, prev, cur, next, w, tmp, 0, 3 * w);
144+
prev = cur;
145+
cur = next;
146+
updateW(tmp, 0, bestRGBY, 0, w);
147+
updateW(tmp, 3 * w, bestRGBY, w, w);
148+
updateChroma(tmp, 0, 3 * w, bestRGBUV, 0, uvWidth);
149+
for (let i = 0; i < 2 * w; i++) {
150+
const diff = targetY[j * w + i] - bestRGBY[i];
151+
bestY[j * w + i] = clip(bestY[j * w + i] + diff);
152+
sum += Math.abs(diff);
153+
}
154+
const uvRow = (j >> 1) * 3 * uvWidth;
155+
for (let i = 0; i < 3 * uvWidth; i++) bestUV[uvRow + i] += targetUV[uvRow + i] - bestRGBUV[i];
156+
}
157+
if (iteration > 0 && (sum < threshold || sum > previousSum)) break;
158+
previousSum = sum;
159+
}
160+
161+
// to YUV
162+
for (let j = 0; j < height; j++) {
163+
const uvRow = (j >> 1) * 3 * uvWidth;
164+
for (let i = 0; i < width; i++) {
165+
const W = bestY[j * w + i], o = uvRow + (i >> 1);
166+
const r = bestUV[o] + W, g = bestUV[o + uvWidth] + W, b = bestUV[o + 2 * uvWidth] + W;
167+
y[j * width + i] = clip8(yuv(r, g, b, 16839, 33059, 6420, 16));
168+
}
169+
}
170+
for (let j = 0; j < uvHeight; j++)
171+
for (let i = 0; i < uvWidth; i++) {
172+
const o = j * 3 * uvWidth + i;
173+
const r = bestUV[o], g = bestUV[o + uvWidth], b = bestUV[o + 2 * uvWidth];
174+
u[j * uvWidth + i] = clip8(yuv(r, g, b, -9719, -19081, 28800, 128));
175+
v[j * uvWidth + i] = clip8(yuv(r, g, b, 28800, -24116, -4684, 128));
176+
}
177+
}

0 commit comments

Comments
 (0)