1 // Copyright 2014 PDFium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
6
7 #include "core/fxge/include/fx_ge.h"
8 #include "core/fxge/include/ifx_renderdevicedriver.h"
9
10 #if defined _SKIA_SUPPORT_
11 #include "third_party/skia/include/core/SkTypes.h"
12 #endif
13
CFX_RenderDevice()14 CFX_RenderDevice::CFX_RenderDevice()
15 : m_pBitmap(nullptr),
16 m_Width(0),
17 m_Height(0),
18 m_bpp(0),
19 m_RenderCaps(0),
20 m_DeviceClass(0),
21 m_pDeviceDriver(nullptr) {}
22
~CFX_RenderDevice()23 CFX_RenderDevice::~CFX_RenderDevice() {
24 delete m_pDeviceDriver;
25 }
26
Flush()27 void CFX_RenderDevice::Flush() {
28 delete m_pDeviceDriver;
29 m_pDeviceDriver = nullptr;
30 }
31
SetDeviceDriver(IFX_RenderDeviceDriver * pDriver)32 void CFX_RenderDevice::SetDeviceDriver(IFX_RenderDeviceDriver* pDriver) {
33 delete m_pDeviceDriver;
34 m_pDeviceDriver = pDriver;
35 InitDeviceInfo();
36 }
37
InitDeviceInfo()38 void CFX_RenderDevice::InitDeviceInfo() {
39 m_Width = m_pDeviceDriver->GetDeviceCaps(FXDC_PIXEL_WIDTH);
40 m_Height = m_pDeviceDriver->GetDeviceCaps(FXDC_PIXEL_HEIGHT);
41 m_bpp = m_pDeviceDriver->GetDeviceCaps(FXDC_BITS_PIXEL);
42 m_RenderCaps = m_pDeviceDriver->GetDeviceCaps(FXDC_RENDER_CAPS);
43 m_DeviceClass = m_pDeviceDriver->GetDeviceCaps(FXDC_DEVICE_CLASS);
44 if (!m_pDeviceDriver->GetClipBox(&m_ClipBox)) {
45 m_ClipBox.left = 0;
46 m_ClipBox.top = 0;
47 m_ClipBox.right = m_Width;
48 m_ClipBox.bottom = m_Height;
49 }
50 }
51
StartRendering()52 FX_BOOL CFX_RenderDevice::StartRendering() {
53 return m_pDeviceDriver->StartRendering();
54 }
55
EndRendering()56 void CFX_RenderDevice::EndRendering() {
57 m_pDeviceDriver->EndRendering();
58 }
59
SaveState()60 void CFX_RenderDevice::SaveState() {
61 m_pDeviceDriver->SaveState();
62 }
63
RestoreState(bool bKeepSaved)64 void CFX_RenderDevice::RestoreState(bool bKeepSaved) {
65 m_pDeviceDriver->RestoreState(bKeepSaved);
66 UpdateClipBox();
67 }
68
GetDeviceCaps(int caps_id) const69 int CFX_RenderDevice::GetDeviceCaps(int caps_id) const {
70 return m_pDeviceDriver->GetDeviceCaps(caps_id);
71 }
GetCTM() const72 CFX_Matrix CFX_RenderDevice::GetCTM() const {
73 return m_pDeviceDriver->GetCTM();
74 }
75
CreateCompatibleBitmap(CFX_DIBitmap * pDIB,int width,int height) const76 FX_BOOL CFX_RenderDevice::CreateCompatibleBitmap(CFX_DIBitmap* pDIB,
77 int width,
78 int height) const {
79 if (m_RenderCaps & FXRC_CMYK_OUTPUT) {
80 return pDIB->Create(width, height, m_RenderCaps & FXRC_ALPHA_OUTPUT
81 ? FXDIB_Cmyka
82 : FXDIB_Cmyk);
83 }
84 if (m_RenderCaps & FXRC_BYTEMASK_OUTPUT) {
85 return pDIB->Create(width, height, FXDIB_8bppMask);
86 }
87 #if _FXM_PLATFORM_ == _FXM_PLATFORM_APPLE_
88 return pDIB->Create(width, height, m_RenderCaps & FXRC_ALPHA_OUTPUT
89 ? FXDIB_Argb
90 : FXDIB_Rgb32);
91 #else
92 return pDIB->Create(
93 width, height, m_RenderCaps & FXRC_ALPHA_OUTPUT ? FXDIB_Argb : FXDIB_Rgb);
94 #endif
95 }
96
SetClip_PathFill(const CFX_PathData * pPathData,const CFX_Matrix * pObject2Device,int fill_mode)97 FX_BOOL CFX_RenderDevice::SetClip_PathFill(const CFX_PathData* pPathData,
98 const CFX_Matrix* pObject2Device,
99 int fill_mode) {
100 if (!m_pDeviceDriver->SetClip_PathFill(pPathData, pObject2Device,
101 fill_mode)) {
102 return FALSE;
103 }
104 UpdateClipBox();
105 return TRUE;
106 }
107
SetClip_PathStroke(const CFX_PathData * pPathData,const CFX_Matrix * pObject2Device,const CFX_GraphStateData * pGraphState)108 FX_BOOL CFX_RenderDevice::SetClip_PathStroke(
109 const CFX_PathData* pPathData,
110 const CFX_Matrix* pObject2Device,
111 const CFX_GraphStateData* pGraphState) {
112 if (!m_pDeviceDriver->SetClip_PathStroke(pPathData, pObject2Device,
113 pGraphState)) {
114 return FALSE;
115 }
116 UpdateClipBox();
117 return TRUE;
118 }
119
SetClip_Rect(const FX_RECT & rect)120 FX_BOOL CFX_RenderDevice::SetClip_Rect(const FX_RECT& rect) {
121 CFX_PathData path;
122 path.AppendRect(rect.left, rect.bottom, rect.right, rect.top);
123 if (!SetClip_PathFill(&path, nullptr, FXFILL_WINDING))
124 return FALSE;
125
126 UpdateClipBox();
127 return TRUE;
128 }
129
UpdateClipBox()130 void CFX_RenderDevice::UpdateClipBox() {
131 if (m_pDeviceDriver->GetClipBox(&m_ClipBox)) {
132 return;
133 }
134 m_ClipBox.left = 0;
135 m_ClipBox.top = 0;
136 m_ClipBox.right = m_Width;
137 m_ClipBox.bottom = m_Height;
138 }
139
DrawPathWithBlend(const CFX_PathData * pPathData,const CFX_Matrix * pObject2Device,const CFX_GraphStateData * pGraphState,uint32_t fill_color,uint32_t stroke_color,int fill_mode,int blend_type)140 FX_BOOL CFX_RenderDevice::DrawPathWithBlend(
141 const CFX_PathData* pPathData,
142 const CFX_Matrix* pObject2Device,
143 const CFX_GraphStateData* pGraphState,
144 uint32_t fill_color,
145 uint32_t stroke_color,
146 int fill_mode,
147 int blend_type) {
148 uint8_t stroke_alpha = pGraphState ? FXARGB_A(stroke_color) : 0;
149 uint8_t fill_alpha = (fill_mode & 3) ? FXARGB_A(fill_color) : 0;
150 if (stroke_alpha == 0 && pPathData->GetPointCount() == 2) {
151 FX_PATHPOINT* pPoints = pPathData->GetPoints();
152 FX_FLOAT x1, x2, y1, y2;
153 if (pObject2Device) {
154 pObject2Device->Transform(pPoints[0].m_PointX, pPoints[0].m_PointY, x1,
155 y1);
156 pObject2Device->Transform(pPoints[1].m_PointX, pPoints[1].m_PointY, x2,
157 y2);
158 } else {
159 x1 = pPoints[0].m_PointX;
160 y1 = pPoints[0].m_PointY;
161 x2 = pPoints[1].m_PointX;
162 y2 = pPoints[1].m_PointY;
163 }
164 DrawCosmeticLineWithFillModeAndBlend(x1, y1, x2, y2, fill_color, fill_mode,
165 blend_type);
166 return TRUE;
167 }
168 if ((pPathData->GetPointCount() == 5 || pPathData->GetPointCount() == 4) &&
169 stroke_alpha == 0) {
170 CFX_FloatRect rect_f;
171 if (!(fill_mode & FXFILL_RECT_AA) &&
172 pPathData->IsRect(pObject2Device, &rect_f)) {
173 FX_RECT rect_i = rect_f.GetOutterRect();
174 int width = (int)FXSYS_ceil(rect_f.right - rect_f.left);
175 if (width < 1) {
176 width = 1;
177 if (rect_i.left == rect_i.right) {
178 rect_i.right++;
179 }
180 }
181 int height = (int)FXSYS_ceil(rect_f.top - rect_f.bottom);
182 if (height < 1) {
183 height = 1;
184 if (rect_i.bottom == rect_i.top) {
185 rect_i.bottom++;
186 }
187 }
188 if (rect_i.Width() >= width + 1) {
189 if (rect_f.left - (FX_FLOAT)(rect_i.left) >
190 (FX_FLOAT)(rect_i.right) - rect_f.right) {
191 rect_i.left++;
192 } else {
193 rect_i.right--;
194 }
195 }
196 if (rect_i.Height() >= height + 1) {
197 if (rect_f.top - (FX_FLOAT)(rect_i.top) >
198 (FX_FLOAT)(rect_i.bottom) - rect_f.bottom) {
199 rect_i.top++;
200 } else {
201 rect_i.bottom--;
202 }
203 }
204 if (FillRectWithBlend(&rect_i, fill_color, blend_type)) {
205 return TRUE;
206 }
207 }
208 }
209 if ((fill_mode & 3) && stroke_alpha == 0 && !(fill_mode & FX_FILL_STROKE) &&
210 !(fill_mode & FX_FILL_TEXT_MODE)) {
211 CFX_PathData newPath;
212 FX_BOOL bThin = FALSE;
213 if (pPathData->GetZeroAreaPath(newPath, (CFX_Matrix*)pObject2Device, bThin,
214 m_pDeviceDriver->GetDriverType())) {
215 CFX_GraphStateData graphState;
216 graphState.m_LineWidth = 0.0f;
217 uint32_t strokecolor = fill_color;
218 if (bThin) {
219 strokecolor = (((fill_alpha >> 2) << 24) | (strokecolor & 0x00ffffff));
220 }
221 CFX_Matrix* pMatrix = nullptr;
222 if (pObject2Device && !pObject2Device->IsIdentity()) {
223 pMatrix = (CFX_Matrix*)pObject2Device;
224 }
225 int smooth_path = FX_ZEROAREA_FILL;
226 if (fill_mode & FXFILL_NOPATHSMOOTH) {
227 smooth_path |= FXFILL_NOPATHSMOOTH;
228 }
229 m_pDeviceDriver->DrawPath(&newPath, pMatrix, &graphState, 0, strokecolor,
230 smooth_path, blend_type);
231 }
232 }
233 if ((fill_mode & 3) && fill_alpha && stroke_alpha < 0xff &&
234 (fill_mode & FX_FILL_STROKE)) {
235 if (m_RenderCaps & FXRC_FILLSTROKE_PATH) {
236 return m_pDeviceDriver->DrawPath(pPathData, pObject2Device, pGraphState,
237 fill_color, stroke_color, fill_mode,
238 blend_type);
239 }
240 return DrawFillStrokePath(pPathData, pObject2Device, pGraphState,
241 fill_color, stroke_color, fill_mode, blend_type);
242 }
243 return m_pDeviceDriver->DrawPath(pPathData, pObject2Device, pGraphState,
244 fill_color, stroke_color, fill_mode,
245 blend_type);
246 }
247
248 // This can be removed once PDFium entirely relies on Skia
DrawFillStrokePath(const CFX_PathData * pPathData,const CFX_Matrix * pObject2Device,const CFX_GraphStateData * pGraphState,uint32_t fill_color,uint32_t stroke_color,int fill_mode,int blend_type)249 FX_BOOL CFX_RenderDevice::DrawFillStrokePath(
250 const CFX_PathData* pPathData,
251 const CFX_Matrix* pObject2Device,
252 const CFX_GraphStateData* pGraphState,
253 uint32_t fill_color,
254 uint32_t stroke_color,
255 int fill_mode,
256 int blend_type) {
257 if (!(m_RenderCaps & FXRC_GET_BITS)) {
258 return FALSE;
259 }
260 CFX_FloatRect bbox;
261 if (pGraphState) {
262 bbox = pPathData->GetBoundingBox(pGraphState->m_LineWidth,
263 pGraphState->m_MiterLimit);
264 } else {
265 bbox = pPathData->GetBoundingBox();
266 }
267 if (pObject2Device) {
268 bbox.Transform(pObject2Device);
269 }
270 CFX_Matrix ctm = GetCTM();
271 FX_FLOAT fScaleX = FXSYS_fabs(ctm.a);
272 FX_FLOAT fScaleY = FXSYS_fabs(ctm.d);
273 FX_RECT rect = bbox.GetOutterRect();
274 CFX_DIBitmap bitmap, Backdrop;
275 if (!CreateCompatibleBitmap(&bitmap, FXSYS_round(rect.Width() * fScaleX),
276 FXSYS_round(rect.Height() * fScaleY))) {
277 return FALSE;
278 }
279 if (bitmap.HasAlpha()) {
280 bitmap.Clear(0);
281 Backdrop.Copy(&bitmap);
282 } else {
283 if (!m_pDeviceDriver->GetDIBits(&bitmap, rect.left, rect.top))
284 return FALSE;
285 Backdrop.Copy(&bitmap);
286 }
287 CFX_FxgeDevice bitmap_device;
288 bitmap_device.Attach(&bitmap, false, &Backdrop, true);
289 CFX_Matrix matrix;
290 if (pObject2Device) {
291 matrix = *pObject2Device;
292 }
293 matrix.TranslateI(-rect.left, -rect.top);
294 matrix.Concat(fScaleX, 0, 0, fScaleY, 0, 0);
295 if (!bitmap_device.GetDeviceDriver()->DrawPath(
296 pPathData, &matrix, pGraphState, fill_color, stroke_color,
297 fill_mode, blend_type)) {
298 return FALSE;
299 }
300 FX_RECT src_rect(0, 0, FXSYS_round(rect.Width() * fScaleX),
301 FXSYS_round(rect.Height() * fScaleY));
302 return m_pDeviceDriver->SetDIBits(&bitmap, 0, &src_rect, rect.left,
303 rect.top, FXDIB_BLEND_NORMAL);
304 }
305
SetPixel(int x,int y,uint32_t color)306 FX_BOOL CFX_RenderDevice::SetPixel(int x, int y, uint32_t color) {
307 if (m_pDeviceDriver->SetPixel(x, y, color))
308 return TRUE;
309
310 FX_RECT rect(x, y, x + 1, y + 1);
311 return FillRectWithBlend(&rect, color, FXDIB_BLEND_NORMAL);
312 }
313
FillRectWithBlend(const FX_RECT * pRect,uint32_t fill_color,int blend_type)314 FX_BOOL CFX_RenderDevice::FillRectWithBlend(const FX_RECT* pRect,
315 uint32_t fill_color,
316 int blend_type) {
317 if (m_pDeviceDriver->FillRectWithBlend(pRect, fill_color, blend_type))
318 return TRUE;
319
320 if (!(m_RenderCaps & FXRC_GET_BITS))
321 return FALSE;
322
323 CFX_DIBitmap bitmap;
324 if (!CreateCompatibleBitmap(&bitmap, pRect->Width(), pRect->Height()))
325 return FALSE;
326
327 if (!m_pDeviceDriver->GetDIBits(&bitmap, pRect->left, pRect->top))
328 return FALSE;
329
330 if (!bitmap.CompositeRect(0, 0, pRect->Width(), pRect->Height(), fill_color,
331 0, nullptr)) {
332 return FALSE;
333 }
334 FX_RECT src_rect(0, 0, pRect->Width(), pRect->Height());
335 m_pDeviceDriver->SetDIBits(&bitmap, 0, &src_rect, pRect->left, pRect->top,
336 FXDIB_BLEND_NORMAL);
337 return TRUE;
338 }
339
DrawCosmeticLineWithFillModeAndBlend(FX_FLOAT x1,FX_FLOAT y1,FX_FLOAT x2,FX_FLOAT y2,uint32_t color,int fill_mode,int blend_type)340 FX_BOOL CFX_RenderDevice::DrawCosmeticLineWithFillModeAndBlend(FX_FLOAT x1,
341 FX_FLOAT y1,
342 FX_FLOAT x2,
343 FX_FLOAT y2,
344 uint32_t color,
345 int fill_mode,
346 int blend_type) {
347 if ((color >= 0xff000000) &&
348 m_pDeviceDriver->DrawCosmeticLine(x1, y1, x2, y2, color, blend_type)) {
349 return TRUE;
350 }
351 CFX_GraphStateData graph_state;
352 CFX_PathData path;
353 path.SetPointCount(2);
354 path.SetPoint(0, x1, y1, FXPT_MOVETO);
355 path.SetPoint(1, x2, y2, FXPT_LINETO);
356 return m_pDeviceDriver->DrawPath(&path, nullptr, &graph_state, 0, color,
357 fill_mode, blend_type);
358 }
359
GetDIBits(CFX_DIBitmap * pBitmap,int left,int top)360 FX_BOOL CFX_RenderDevice::GetDIBits(CFX_DIBitmap* pBitmap, int left, int top) {
361 if (!(m_RenderCaps & FXRC_GET_BITS))
362 return FALSE;
363 return m_pDeviceDriver->GetDIBits(pBitmap, left, top);
364 }
365
GetBackDrop()366 CFX_DIBitmap* CFX_RenderDevice::GetBackDrop() {
367 return m_pDeviceDriver->GetBackDrop();
368 }
369
SetDIBitsWithBlend(const CFX_DIBSource * pBitmap,int left,int top,int blend_mode)370 FX_BOOL CFX_RenderDevice::SetDIBitsWithBlend(const CFX_DIBSource* pBitmap,
371 int left,
372 int top,
373 int blend_mode) {
374 ASSERT(!pBitmap->IsAlphaMask());
375 CFX_Matrix ctm = GetCTM();
376 FX_FLOAT fScaleX = FXSYS_fabs(ctm.a);
377 FX_FLOAT fScaleY = FXSYS_fabs(ctm.d);
378 FX_RECT dest_rect(left, top,
379 FXSYS_round(left + pBitmap->GetWidth() / fScaleX),
380 FXSYS_round(top + pBitmap->GetHeight() / fScaleY));
381 dest_rect.Intersect(m_ClipBox);
382 if (dest_rect.IsEmpty()) {
383 return TRUE;
384 }
385 FX_RECT src_rect(dest_rect.left - left, dest_rect.top - top,
386 dest_rect.left - left + dest_rect.Width(),
387 dest_rect.top - top + dest_rect.Height());
388 src_rect.left = FXSYS_round(src_rect.left * fScaleX);
389 src_rect.top = FXSYS_round(src_rect.top * fScaleY);
390 src_rect.right = FXSYS_round(src_rect.right * fScaleX);
391 src_rect.bottom = FXSYS_round(src_rect.bottom * fScaleY);
392 if ((blend_mode != FXDIB_BLEND_NORMAL && !(m_RenderCaps & FXRC_BLEND_MODE)) ||
393 (pBitmap->HasAlpha() && !(m_RenderCaps & FXRC_ALPHA_IMAGE))) {
394 if (!(m_RenderCaps & FXRC_GET_BITS)) {
395 return FALSE;
396 }
397 int bg_pixel_width = FXSYS_round(dest_rect.Width() * fScaleX);
398 int bg_pixel_height = FXSYS_round(dest_rect.Height() * fScaleY);
399 CFX_DIBitmap background;
400 if (!background.Create(
401 bg_pixel_width, bg_pixel_height,
402 (m_RenderCaps & FXRC_CMYK_OUTPUT) ? FXDIB_Cmyk : FXDIB_Rgb32)) {
403 return FALSE;
404 }
405 if (!m_pDeviceDriver->GetDIBits(&background, dest_rect.left,
406 dest_rect.top)) {
407 return FALSE;
408 }
409 if (!background.CompositeBitmap(0, 0, bg_pixel_width, bg_pixel_height,
410 pBitmap, src_rect.left, src_rect.top,
411 blend_mode, nullptr, FALSE, nullptr)) {
412 return FALSE;
413 }
414 FX_RECT rect(0, 0, bg_pixel_width, bg_pixel_height);
415 return m_pDeviceDriver->SetDIBits(&background, 0, &rect, dest_rect.left,
416 dest_rect.top, FXDIB_BLEND_NORMAL);
417 }
418 return m_pDeviceDriver->SetDIBits(pBitmap, 0, &src_rect, dest_rect.left,
419 dest_rect.top, blend_mode);
420 }
421
StretchDIBitsWithFlagsAndBlend(const CFX_DIBSource * pBitmap,int left,int top,int dest_width,int dest_height,uint32_t flags,int blend_mode)422 FX_BOOL CFX_RenderDevice::StretchDIBitsWithFlagsAndBlend(
423 const CFX_DIBSource* pBitmap,
424 int left,
425 int top,
426 int dest_width,
427 int dest_height,
428 uint32_t flags,
429 int blend_mode) {
430 FX_RECT dest_rect(left, top, left + dest_width, top + dest_height);
431 FX_RECT clip_box = m_ClipBox;
432 clip_box.Intersect(dest_rect);
433 if (clip_box.IsEmpty())
434 return TRUE;
435 return m_pDeviceDriver->StretchDIBits(pBitmap, 0, left, top, dest_width,
436 dest_height, &clip_box, flags,
437 blend_mode);
438 }
439
SetBitMask(const CFX_DIBSource * pBitmap,int left,int top,uint32_t argb)440 FX_BOOL CFX_RenderDevice::SetBitMask(const CFX_DIBSource* pBitmap,
441 int left,
442 int top,
443 uint32_t argb) {
444 FX_RECT src_rect(0, 0, pBitmap->GetWidth(), pBitmap->GetHeight());
445 return m_pDeviceDriver->SetDIBits(pBitmap, argb, &src_rect, left, top,
446 FXDIB_BLEND_NORMAL);
447 }
448
StretchBitMask(const CFX_DIBSource * pBitmap,int left,int top,int dest_width,int dest_height,uint32_t color)449 FX_BOOL CFX_RenderDevice::StretchBitMask(const CFX_DIBSource* pBitmap,
450 int left,
451 int top,
452 int dest_width,
453 int dest_height,
454 uint32_t color) {
455 return StretchBitMaskWithFlags(pBitmap, left, top, dest_width, dest_height,
456 color, 0);
457 }
458
StretchBitMaskWithFlags(const CFX_DIBSource * pBitmap,int left,int top,int dest_width,int dest_height,uint32_t argb,uint32_t flags)459 FX_BOOL CFX_RenderDevice::StretchBitMaskWithFlags(const CFX_DIBSource* pBitmap,
460 int left,
461 int top,
462 int dest_width,
463 int dest_height,
464 uint32_t argb,
465 uint32_t flags) {
466 FX_RECT dest_rect(left, top, left + dest_width, top + dest_height);
467 FX_RECT clip_box = m_ClipBox;
468 clip_box.Intersect(dest_rect);
469 return m_pDeviceDriver->StretchDIBits(pBitmap, argb, left, top, dest_width,
470 dest_height, &clip_box, flags,
471 FXDIB_BLEND_NORMAL);
472 }
473
StartDIBitsWithBlend(const CFX_DIBSource * pBitmap,int bitmap_alpha,uint32_t argb,const CFX_Matrix * pMatrix,uint32_t flags,void * & handle,int blend_mode)474 FX_BOOL CFX_RenderDevice::StartDIBitsWithBlend(const CFX_DIBSource* pBitmap,
475 int bitmap_alpha,
476 uint32_t argb,
477 const CFX_Matrix* pMatrix,
478 uint32_t flags,
479 void*& handle,
480 int blend_mode) {
481 return m_pDeviceDriver->StartDIBits(pBitmap, bitmap_alpha, argb, pMatrix,
482 flags, handle, blend_mode);
483 }
484
ContinueDIBits(void * handle,IFX_Pause * pPause)485 FX_BOOL CFX_RenderDevice::ContinueDIBits(void* handle, IFX_Pause* pPause) {
486 return m_pDeviceDriver->ContinueDIBits(handle, pPause);
487 }
488
CancelDIBits(void * handle)489 void CFX_RenderDevice::CancelDIBits(void* handle) {
490 m_pDeviceDriver->CancelDIBits(handle);
491 }
492
493 #ifdef _SKIA_SUPPORT_
494
DebugVerifyBitmapIsPreMultiplied() const495 void CFX_RenderDevice::DebugVerifyBitmapIsPreMultiplied() const {
496 SkASSERT(0);
497 }
498 #endif
499