add Stroke node

This commit is contained in:
chflame163
2024-01-19 18:02:17 +08:00
committed by GitHub
parent 1b5f10d8c6
commit 570612e2c3
13 changed files with 471 additions and 70 deletions
+71 -28
View File
@@ -1,48 +1,45 @@
# ComfyUI Layer Style
A set of nodes for ComfyUI that generate image like Adobe Photoshop's Layer Style.
Currently completed Drop Shadow and OuterGlow nodes, and follow-up work is in progress.
Currently completed the Drop Shadow, OuterGlow, and some auxiliary nodes. follow-up work is in progress.
[中文说明点这里](./README_CN.MD)
## Update
* Commit Stroke node.
* Commit MaskPreview node.
* Commit ImageOpacity node.
* The layer_mask is not a mandatory input. without inputting will use the layer_image's alpha channel. It is allowed to use layers and masks with different shapes, but the size must be consistent.
![image](image/title.png)
* The layer_mask is not a mandatory input now. it is allowed to use layers and masks with different shapes, but the size must be consistent.
* Commit ImageBlend node.
* Remove the mask channel output from the DropShadow node; Commit OuterGlow node.
* Commit OuterGlow node.
* Commit DropShadow node.
## Node Description
### Drop Shadow:
### DropShadow:
Generate shadow
![image](image/drop_shadow_example.png)
Node options:
![image](image/drop_shadow_node.png)
* background_image<sup>1</sup>: The background image.
* layer_image<sup>1</sup>: Layer image for composite.
* layer_mask<sup>1</sup>: Mask for layer_image, shadows are generated according to their shape.
* layer_mask<sup>1,2</sup>: Mask for layer_image, shadows are generated according to their shape.
* invert_mask: Whether to reverse the mask.
* blend_mode: Blending method of shadows, there are **_normal, multply, screen, add, subtract, difference, darker_** and **_lighter_**.
* blend_mode<sup>3</sup>: Blending mode of shadows.
* opacity: Opacity of shadow.
* distance_x: Horizontal offset of shadow.
* distance_y: Vertical offset of shadow.
* grow: Shadow expansion amplitude.
* blur:Shadow blur level.
* blur: Shadow blur level.
* shadow_color: Shadow color, described in hexadecimal RGB format.
<sup>1</sup> The layer_image, layer_mask and the background_image, These three items must be of the same size.
Output type:
* image
### OuterGlow:
### OuterGlow:
Generate outer glow
![image](image/outer_glow_example.png)
@@ -50,20 +47,40 @@ Node options:
![image](image/outer_glow_node.png)
* background_image<sup>1</sup>: The background image.
* layer_image<sup>1</sup>: Layer image for composite.
* layer_mask<sup>1</sup>: Mask for layer_image, shadows are generated according to their shape.
* layer_mask<sup>1,2</sup><sup>2</sup>: Mask for layer_image, shadows are generated according to their shape.
* invert_mask: Whether to reverse the mask.
* blend_mode: Blending method of glow, there are **_normal, multply, screen, add, subtract, difference, darker_** and **_lighter_**.
* opacity: Opacity of Glow.
* blend_mode<sup>3</sup>: Blending mode of glow.
* opacity: Opacity of glow.
* grow: Glow expansion amplitude.
* blur:Gkow blur level.
* glow_color: Glow color, described in hexadecimal RGB format.
* blur: Gkow blur level.
* glow_color: Glow color, described in hexadecimal RGB format.
<sup>1</sup> The layer_image, layer_mask and the background_image, These three items must be of the same size.
Output type:
* image
### ImageBlend:
### Stroke:
Generate a stroke of layer。
![image](image/stroke_example.png)
Node options:
![image](image/stroke_node.png)
* background_image<sup>1</sup>: The background image.
* layer_image<sup>1</sup>: Layer image for composite.
* layer_mask<sup>1,2</sup><sup>2</sup>: Mask for layer_image, shadows are generated according to their shape.
* invert_mask: Whether to reverse the mask.
* blend_mode<sup>3</sup>: Blending mode of stroke.
* opacity: Opacity of stroke.
* shrink<sup>*</sup>: Stroke shrinkage amplitude.
* expansion<sup>*</sup>: Stroke expansion amplitude.
* stroke_color: Stroke color, described in hexadecimal RGB format.
<sup>*</sup>The width of the stroke is equal to shrink plus expansion value. Setting two values separately can make it more convenient to adjust the border range.
Output type:
* image
### ImageBlend:
Composit layer image and background image, multiple blend modes are available for option, and transparency can be set.
![image](image/image_blend_example.png)
@@ -71,24 +88,21 @@ Node options:
![image](image/image_blend_node.png)
* background_image<sup>1</sup>: The background image.
* layer_image<sup>1</sup>: Layer image for composite.
* layer_mask<sup>1</sup>: Mask for layer_image.
* layer_mask<sup>1,2</sup>: Mask for layer_image.
* invert_mask: Whether to reverse the mask.
* blend_mode: Blending method of glow, there are **_normal, multply, screen, add, subtract, difference, darker_** and **_lighter_**.
* blend_mode<sup>3</sup>: Blending mode.
* opacity: Opacity of blend.
<sup>1</sup> The layer_image, layer_mask and the background_image, These three items must be of the same size.。
Output type:
* image
### ImageOpacity:
### ImageOpacity:
Adjust image opacity
![image](image/image_opacity_example.png)
Node option:
* image: Image input, supporting RGB and RGBA. if is RGB, the alpha channel of the entire image will be automatically added.
* mask: Mask input. this is not a mandatory input item. attention that if the image input has alpha channel and different masks are input simultaneously, the original alpha channel will be overwritten by the mask.
* image<sup>1</sup>: Image input, supporting RGB and RGBA. if is RGB, the alpha channel of the entire image will be automatically added.
* mask<sup>1,2</sup> : Mask input.
* invert_mask: Whether to reverse the mask.
* opacity: Opacity of image.
@@ -96,9 +110,38 @@ Output type:
* image(RGBA)
* mask
### MaskPreview:
Preview the input mask
![image](image/mask_preview_example.png)
input:mask
### Annotation:
<sup>1</sup> The layer_image, layer_mask and the background_image(if have input), These three items must be of the same size.。
<sup>2</sup> The mask not a mandatory input item. the alpha channel of the image is used by default. If the image input does not include an alpha channel, the entire image's alpha channel will be automatically created. if have masks input simultaneously, the alpha channel will be overwrite by the mask.
<sup>3</sup> The blend mode include **_normal, multply, screen, add, subtract, difference, darker_** and **_lighter_**.
## Example workflow
image Some JSON workflow files in the workflow directory, that is example for ComfyUI.
## The following tasks are in the planned list
### Layer Style node:
* Inner Shadow
* Inner Glow
* Texture
* Color Overlay
* Gradient Overlay
* Neon
### Auxiliary node:
* invert mask
* mask soft crop
* mask motion blur
* mask radial blur
## How to install
* Open the cmd window in the plugin directory of ComfyUI, like "ComfyUI\custom_nodes\",type```git clone https://github.com/chflame163/ComfyUI_LayerStyle.git```
+66 -18
View File
@@ -1,14 +1,16 @@
# ComfyUI Layer Style:
ComfyUI插件,通过一组节点实现仿照Adobe Photoshop的图层样式。
当前已完成Drop Shadow和OuterGlow节点,后续工作进行中。
当前已完成Drop Shadow和OuterGlow节点和一些辅助节点,后续工作进行中。
## 更新说明:
* 添加Stroke节点。
* 添加MaskPreview节点。
* 添加ImageOpacity节点。
* layer_mask不是输入必选。不输入将使用layer_image的alpha通道。允许使用形状不同的layer和mask,但是尺寸必须一致。
![image](image/title.png)
* 添加ImageBlend节点
* DropShadow节点去掉调试mask通道输出;添加OuterGlow节点。
* DropShadow节点上线。
* layer_mask修改为非必选, 默认使用layer_image的alpha通道,允许通过输入mask改变之,但是尺寸必须一致。
* 添加ImageBlend节点。
* 添加OuterGlow节点。
* 首个节点DropShadow提交。
## 节点说明:
@@ -17,14 +19,13 @@ ComfyUI
![image](image/drop_shadow_example.png)
节点选项说明:
![image](image/drop_shadow_node.png)
* background_image<sup>1</sup>: 背景图像。
* layer_image<sup>1</sup>: 用于合成的层图像。
* layer_mask<sup>1</sup>: 层图像的遮罩,阴影按此生成。
* layer_mask<sup>1,2</sup>: 层图像的遮罩,阴影按此生成。
* invert_mask: 是否反转遮罩。
* blend_mode: 阴影的混合模式,包括normal、multply、screen、add、subtract、difference、darker和lighter。
* blend_mode<sup>3</sup>: 阴影的混合模式。
* opacity: 阴影的不透明度。
* distance_x: 阴影的水平方向偏移量。
* distance_y: 阴影的垂直方向偏移量。
@@ -32,7 +33,6 @@ ComfyUI
* blur:阴影模糊程度。
* shadow_color: 阴影颜色,使用16进制RGB格式描述。
<sup>1</sup> background_image, layer_image, layer_mask这三项必须是相同的尺寸。
输出:
* image
@@ -47,13 +47,34 @@ ComfyUI
* layer_image<sup>1</sup>: 用于合成的层图像。
* layer_mask<sup>1</sup>: 层图像的遮罩,外发光按此生成。
* invert_mask: 是否反转遮罩。
* blend_mode: 发光的混合模式,包括normal、multply、screen、add、subtract、difference、darker和lighter。
* blend_mode<sup>3</sup>: 发光的混合模式。
* opacity: 发光的不透明度。
* grow: 发光扩张幅度。
* blur:发光模糊程度。
* glow_color: 发光颜色,使用16进制RGB格式描述。
<sup>1</sup> background_image, layer_image, layer_mask这三项必须是相同的尺寸。
输出:
* image
### Stroke:
生成描边。
![image](image/stroke_example.png)
节点选项说明:
![image](image/stroke_node.png)
* background_image<sup>1</sup>: 背景图像。
* layer_image<sup>1</sup>: 用于合成的层图像。
* layer_mask<sup>1</sup>: 层图像的遮罩,描边按此生成。
* invert_mask: 是否反转遮罩。
* blend_mode<sup>3</sup>: 描边的混合模式。
* opacity: 不透明度。
* shrink<sup>*</sup>: 描边收缩幅度。
* expansion<sup>*</sup>:描边扩张幅度。
* stroke_color: 描边颜色,使用16进制RGB格式描述。
<sup>*</sup>描边的宽度等于收缩值加上扩张值。两个值分开设置能更方便的调整边框范围。
输出:
* image
@@ -68,22 +89,20 @@ ComfyUI
* layer_image<sup>1</sup>: 用于合成的层图像。
* layer_mask<sup>1</sup>: 层图像的遮罩。
* invert_mask: 是否反转遮罩。
* blend_mode: 图层混合模式,包括normal、multply、screen、add、subtract、difference、darker和lighter。
* blend_mode<sup>3</sup>: 图层混合模式。
* opacity: 不透明度。
<sup>1</sup> background_image, layer_image, layer_mask这三项必须是相同的尺寸。
输出:
* image
### ImageOpacity:
### ImageOpacity:
调整图像不透明度。
![image](image/image_opacity_example.png)
节点选项说明:
* image: 图像输入,支持RGB和RGBA输入。如果输入的是RGB,将自动添加整个图像的alpha通道。
* mask: 遮罩输入,这不是必须的输入项。注意如果image输入有alpha通道且同时输入了不同的mask,原本的alpha通道将被mask覆盖。
* image: 图像输入,支持RGB和RGBA输入。
* mask: 遮罩输入。
* invert_mask: 是否反转遮罩。
* opacity: 不透明度。
@@ -91,10 +110,39 @@ ComfyUI
* image(RGBA)
* mask
### MaskPreview:
预览mask
![image](image/mask_preview_example.png)
输入:mask
### 节点选项注解:
<sup>1</sup> image、mask和background_image(如果有输入)这三项必须是相同的尺寸。
<sup>2</sup> mask不是必须的输入项,默认使用image的alpha通道,如果image输入不包含alpha通道将自动创建整个图像的alpha通道。如果输入mask,原本的alpha通道将被mask覆盖。
<sup>3</sup>: 混合模式包括normal、multply、screen、add、subtract、difference、darker和lighter。
## 使用示例:
在workflow目录下有json格式的工作流示例文件。
## 以下工作在计划列表中
### Layer Style节点:
* Inner Shadow
* Inner Glow
* Stroke
* Texture
* Color Overlay
* Gradient Overlay
* Neon
### 辅助节点:
* invert mask
* invert image
* load image+
* mask soft crop
* mask motion blur
* mask radial blur
## 安装方法:
* 在CompyUI插件目录(例如“CompyUI\custom_nodes\”)中打开cmd窗口,键入```git clone https://github.com/chflame163/ComfyUI_LayerStyle.git```安装。或者下载解压zip文件,将得到的文件夹复制到 ComfyUI\custom_nodes\
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

+1 -1
View File
@@ -52,7 +52,7 @@ class DropShadow:
if distance_x != 0 or distance_y != 0:
__mask = shift_image(_mask, distance_x, distance_y) # 位移
shadow_mask = expand_mask(image2mask(__mask), grow, blur, 0) #扩张,模糊
shadow_mask = expand_mask(image2mask(__mask), grow, blur) #扩张,模糊
# 合成阴影
shadow_color = Image.new("RGB", _layer.size, color=shadow_color)
+1 -4
View File
@@ -12,7 +12,6 @@ class ImageBlend:
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
@@ -43,11 +42,9 @@ class ImageBlend:
# 合成layer
_comp = chop_image(_canvas, _layer, blend_mode, opacity)
_canvas.paste(_comp, mask=_mask)
ret_image = _canvas
return (pil2tensor(ret_image),)
NODE_CLASS_MAPPINGS = {
+2 -3
View File
@@ -31,15 +31,14 @@ class ImageOpacity:
_image = tensor2pil(image).convert('RGB')
_mask = tensor2pil(image).convert('RGBA').split()[-1]
if mask is not None:
_mask = mask2image(mask).convert('L')
# 设置透明度
if invert_mask:
_color = Image.new("L", _image.size, color=(255))
else:
_color = Image.new("L", _image.size, color=(0))
# opacity
ret_mask = _mask
if opacity == 0:
ret_mask = _color
+29 -14
View File
@@ -1,12 +1,24 @@
'''Image process functions for ComfyUI nodes
by chflame https://github.com/chflame163
'''
import numpy as np
import torch
import scipy.ndimage
import cv2
from typing import Union, List
from PIL import Image, ImageFilter, ImageChops
def log(message):
name = 'Layer Style'
print(f"# 😺dzNodes: {name} -> {message}")
name = 'LayerStyle'
print(f"# 😺dzNodes: {name} -> {message}")
def cv22pil(cv2_img:np.ndarray) -> Image:
cv2_img = cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB)
return Image.fromarray(cv2_img)
def pil2cv2(pil_img:Image) -> np.array:
np_img_array = np.asarray(pil_img)
return cv2.cvtColor(np_img_array, cv2.COLOR_RGB2BGR)
def pil2tensor(image:Image) -> torch.Tensor:
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
@@ -51,7 +63,6 @@ def shift_image(image:Image, distance_x:int, distance_y:int) -> Image:
for y in range(height):
if x > -distance_x and y > -distance_y:
if x + distance_x < width and y + distance_y < height:
# print(f"x={x}, y={y}, distance_x={distance_x}, distance_y={distance_y}")
pixel = image.getpixel((x + distance_x, y + distance_y))
ret_image.putpixel((x, y), pixel)
return ret_image
@@ -85,7 +96,7 @@ def chop_image(background_image:Image, layer_image:Image, blend_mode:str, opacit
return ret_image
def expand_mask(mask:torch.Tensor, grow:int, blur:int, expandrate:int) -> torch.Tensor:
def expand_mask(mask:torch.Tensor, grow:int, blur:int) -> torch.Tensor:
# grow
c = 0
kernel = np.array([[c, 1, c],
@@ -100,19 +111,23 @@ def expand_mask(mask:torch.Tensor, grow:int, blur:int, expandrate:int) -> torch.
output = scipy.ndimage.grey_erosion(output, footprint=kernel)
else:
output = scipy.ndimage.grey_dilation(output, footprint=kernel)
if grow < 0:
grow -= abs(expandrate)
else:
grow += abs(expandrate)
output = torch.from_numpy(output)
out.append(output)
# blur
if blur != 0:
for idx, tensor in enumerate(out):
pil_image = tensor2pil(tensor.cpu().detach())
pil_image = pil_image.filter(ImageFilter.GaussianBlur(blur))
out[idx] = pil2tensor(pil_image)
for idx, tensor in enumerate(out):
pil_image = tensor2pil(tensor.cpu().detach())
pil_image = pil_image.filter(ImageFilter.GaussianBlur(blur))
out[idx] = pil2tensor(pil_image)
ret_mask = torch.cat(out, dim=0)
# ret_mask = torch.tensor([ret_mask.tolist()])
return ret_mask
def remove_background(image:Image, mask:Image, color:str) -> Image:
width = image.width
height = image.height
ret_image = Image.new('RGB', size=(width, height), color=color)
ret_image.paste(image, mask=mask)
return ret_image
def subtract_mask(masks_a, masks_b):
return torch.clamp(masks_a - masks_b, 0, 255)
+32
View File
@@ -0,0 +1,32 @@
import random
from .imagefunc import *
from nodes import SaveImage
import folder_paths
class MaskPreview(SaveImage):
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz1234567890") for x in range(5))
self.compress_level = 4
@classmethod
def INPUT_TYPES(self):
return {
"required": {"mask": ("MASK",), },
}
FUNCTION = "mask_preview"
CATEGORY = '😺dzNodes'
def mask_preview(self, mask):
preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
return self.save_images(preview, "MaskPreview")
NODE_CLASS_MAPPINGS = {
"LayerStyle_MaskPreview": MaskPreview
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle_MaskPreview": "LayerStyle: MaskPreview"
}
-2
View File
@@ -48,7 +48,6 @@ class OuterGlow:
_mask = mask2image(layer_mask).convert('L')
glow_mask = expand_mask(image2mask(_mask), grow, blur, 0) #扩张,模糊
# 合成glow
glow_color = Image.new("RGB", _layer.size, color=glow_color)
alpha = tensor2pil(glow_mask).convert('L')
@@ -57,7 +56,6 @@ class OuterGlow:
# 合成layer
_canvas.paste(_layer, mask=_mask)
ret_image = _canvas
# ret_mask = glow_mask
# return (pil2tensor(ret_image), ret_mask,)
+64
View File
@@ -0,0 +1,64 @@
from .imagefunc import *
class Stroke:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
chop_mode = ['normal','multply','screen','add','subtract','difference','darker','lighter']
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
"shrink": ("INT", {"default": 1, "min": 0, "max": 9999, "step": 1}), # 收缩值
"expansion": ("INT", {"default": 1, "min": 0, "max": 9999, "step": 1}), # 扩张值
"blur": ("INT", {"default": 5, "min": 0, "max": 100, "step": 1}), # 模糊
"stroke_color": ("STRING", {"default": "#FF0000"}), # 描边颜色
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'stroke'
CATEGORY = '😺dzNodes'
OUTPUT_NODE = True
def stroke(self, background_image, layer_image,
invert_mask, blend_mode, opacity,
shrink, expansion, blur, stroke_color,
layer_mask=None,
):
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image).convert('RGB')
_mask = tensor2pil(layer_image).convert('RGBA').split()[-1]
if layer_mask is not None:
if invert_mask:
layer_mask = 1 - layer_mask
_mask = mask2image(layer_mask).convert('L')
shink_mask = expand_mask(image2mask(_mask), -shrink, blur)
expansion_mask = expand_mask(image2mask(_mask), expansion, blur)
strock_mask = subtract_mask(expansion_mask, shink_mask)
color_image = Image.new('RGB', size=_layer.size, color=stroke_color)
blend_image = chop_image(_layer, color_image, blend_mode, opacity)
_canvas.paste(_layer, mask=_mask)
_canvas.paste(blend_image, mask=tensor2pil(strock_mask))
ret_image = _canvas
return (pil2tensor(ret_image),)
NODE_CLASS_MAPPINGS = {
"LayerStyle_Stroke": Stroke
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle_Stroke": "LayerStyle: Stroke"
}
+205
View File
@@ -0,0 +1,205 @@
{
"last_node_id": 15,
"last_link_id": 18,
"nodes": [
{
"id": 1,
"type": "LayerStyle_Stroke",
"pos": [
758,
309
],
"size": [
315,
242
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "background_image",
"type": "IMAGE",
"link": 16
},
{
"name": "layer_image",
"type": "IMAGE",
"link": 10
},
{
"name": "layer_mask",
"type": "MASK",
"link": 3
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
4
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "LayerStyle_Stroke"
},
"widgets_values": [
true,
"normal",
100,
8,
2,
0,
"#DB43A2"
]
},
{
"id": 3,
"type": "LoadImage",
"pos": [
413,
284
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
10
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": [
3
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"512x512 (10).png",
"image"
]
},
{
"id": 2,
"type": "LoadImage",
"pos": [
59,
279
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
16
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"512x512bkgd (7).jpg",
"image"
]
},
{
"id": 4,
"type": "PreviewImage",
"pos": [
1105,
285
],
"size": [
299.70625443891913,
311.19479890303114
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 4
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
}
],
"links": [
[
3,
3,
1,
1,
2,
"MASK"
],
[
4,
1,
0,
4,
0,
"IMAGE"
],
[
10,
3,
0,
1,
1,
"IMAGE"
],
[
16,
2,
0,
1,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}