diff --git a/experiments/svg_cairo.nim b/experiments/svg_cairo.nim index 6d67afd..fce1cb0 100644 --- a/experiments/svg_cairo.nim +++ b/experiments/svg_cairo.nim @@ -95,33 +95,14 @@ proc prepare( c.setFillRule(FillRuleEvenOdd) c.processCommands(path) -proc fillPath( - c: ptr Context, - path: Path, - color: ColorRGBA, - mat: Mat3, - windingRule = wrNonZero -) = - prepare(c, path, color, mat, windingRule) - c.fill() - -proc strokePath( - c: ptr Context, - path: Path, - color: ColorRGBA, - mat: Mat3, - strokeWidth: float32 -) = - prepare(c, path, color, mat) - c.setLineWidth(strokeWidth) - c.stroke() - type Ctx = object fillRule: WindingRule fill, stroke: ColorRGBA strokeWidth: float32 strokeLineCap: paths.LineCap strokeLineJoin: paths.LineJoin + strokeMiterLimit: float32 + strokeDashArray: seq[float32] transform: Mat3 shouldStroke: bool @@ -138,10 +119,18 @@ proc initCtx(): Ctx = result.stroke = parseHtmlColor("black").rgba result.strokeWidth = 1 result.transform = mat3() + result.strokeMiterLimit = defaultMiterLimit proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = result = inherited + proc splitArgs(s: string): seq[string] = + # Handles (1,1) or (1 1) or (1, 1) or (1,1 2,2) etc + let tmp = s.replace(',', ' ').split(' ') + for entry in tmp: + if entry.len > 0: + result.add(entry) + var fillRule = node.attr("fill-rule") fill = node.attr("fill") @@ -149,6 +138,8 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = strokeWidth = node.attr("stroke-width") strokeLineCap = node.attr("stroke-linecap") strokeLineJoin = node.attr("stroke-linejoin") + strokeMiterLimit = node.attr("stroke-miterlimit") + strokeDashArray = node.attr("stroke-dasharray") transform = node.attr("transform") style = node.attr("style") @@ -173,6 +164,12 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = of "stroke-width": if strokeWidth.len == 0: strokeWidth = parts[1].strip() + of "stroke-miterlimit": + if strokeMiterLimit.len == 0: + strokeMiterLimit = parts[1].strip() + of "stroke-dasharray": + if strokeDashArray.len == 0: + strokeDashArray = parts[1].strip() if fillRule == "": discard # Inherit @@ -247,6 +244,18 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = PixieError, "Invalid stroke-linejoin value " & strokeLineJoin ) + if strokeMiterLimit == "": + discard + else: + result.strokeMiterLimit = parseFloat(strokeMiterLimit) + + if strokeDashArray == "": + discard + else: + var values = splitArgs(strokeDashArray) + for value in values: + result.strokeDashArray.add(parseFloat(value)) + if transform == "": discard # Inherit else: @@ -264,46 +273,83 @@ proc decodeCtx(inherited: Ctx, node: XmlNode): Ctx = remaining = remaining[index + 1 .. ^1] if f.startsWith("matrix("): - let arr = - if f.contains(","): - f[7 .. ^2].split(",") - else: - f[7 .. ^2].split(" ") + let arr = splitArgs(f[7 .. ^2]) if arr.len != 6: failInvalidTransform(transform) var m = mat3() - m[0] = parseFloat(arr[0].strip()) - m[1] = parseFloat(arr[1].strip()) - m[3] = parseFloat(arr[2].strip()) - m[4] = parseFloat(arr[3].strip()) - m[6] = parseFloat(arr[4].strip()) - m[7] = parseFloat(arr[5].strip()) + m[0, 0] = parseFloat(arr[0]) + m[0, 1] = parseFloat(arr[1]) + m[1, 0] = parseFloat(arr[2]) + m[1, 1] = parseFloat(arr[3]) + m[2, 0] = parseFloat(arr[4]) + m[2, 1] = parseFloat(arr[5]) result.transform = result.transform * m elif f.startsWith("translate("): let - components = f[10 .. ^2].split(" ") - tx = parseFloat(components[0].strip()) + components = splitArgs(f[10 .. ^2]) + tx = parseFloat(components[0]) ty = - if components[1].len == 0: + if components.len == 1: 0.0 else: - parseFloat(components[1].strip()) + parseFloat(components[1]) result.transform = result.transform * translate(vec2(tx, ty)) elif f.startsWith("rotate("): let - values = f[7 .. ^2].split(" ") - angle = parseFloat(values[0].strip()) * -PI / 180 + values = splitArgs(f[7 .. ^2]) + angle: float32 = parseFloat(values[0]) * -PI / 180 var cx, cy: float32 if values.len > 1: - cx = parseFloat(values[1].strip()) + cx = parseFloat(values[1]) if values.len > 2: - cy = parseFloat(values[2].strip()) + cy = parseFloat(values[2]) let center = vec2(cx, cy) result.transform = result.transform * - translate(center) * rotationMat3(angle) * translate(-center) + translate(center) * rotate(angle) * translate(-center) + elif f.startsWith("scale("): + let + values = splitArgs(f[6 .. ^2]) + sx: float32 = parseFloat(values[0]) + sy: float32 = + if values.len > 1: + parseFloat(values[1]) + else: + sx + result.transform = result.transform * scale(vec2(sx, sy)) else: failInvalidTransform(transform) +proc cairoLineCap(lineCap: paths.LineCap): cairo.LineCap = + case lineCap: + of lcButt: + LineCapButt + of lcRound: + LineCapRound + of lcSquare: + LineCapSquare + +proc cairoLineJoin(lineJoin: paths.LineJoin): cairo.LineJoin = + case lineJoin: + of ljMiter: + LineJoinMiter + of ljBevel: + LineJoinBevel + of ljRound: + LineJoinRound + +proc fill(c: ptr Context, ctx: Ctx, path: Path) {.inline.} = + # img.fillPath(path, ctx.fill, ctx.transform, ctx.fillRule) + prepare(c, path, ctx.fill, ctx.transform, ctx.fillRule) + c.fill() + +proc stroke(c: ptr Context, ctx: Ctx, path: Path) {.inline.} = + prepare(c, path, ctx.stroke, ctx.transform) + c.setLineWidth(ctx.strokeWidth) + c.setLineCap(ctx.strokeLineCap.cairoLineCap()) + c.setLineJoin(ctx.strokeLineJoin.cairoLineJoin()) + c.setMiterLimit(ctx.strokeMiterLimit) + c.stroke() + proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = if node.kind != xnElement: # Skip @@ -326,9 +372,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = ctx = decodeCtx(ctxStack[^1], node) path = parsePath(d) if ctx.fill != ColorRGBA(): - img.fillPath(path, ctx.fill, ctx.transform, ctx.fillRule) + img.fill(ctx, path) if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) of "line": let @@ -341,12 +387,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = var path: Path path.moveTo(x1, y1) path.lineTo(x2, y2) - path.closePath() - if ctx.fill != ColorRGBA(): - img.fillPath(path, ctx.fill, ctx.transform) if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) of "polyline", "polygon": let @@ -376,21 +419,26 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = path.lineTo(vecs[i]) # The difference between polyline and polygon is whether we close the path + # and fill or not if node.tag == "polygon": path.closePath() - if ctx.fill != ColorRGBA(): - img.fillPath(path, ctx.fill, ctx.transform) + if ctx.fill != ColorRGBA(): + img.fill(ctx, path) + if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) of "rect": let ctx = decodeCtx(ctxStack[^1], node) x = parseFloat(node.attrOrDefault("x", "0")) y = parseFloat(node.attrOrDefault("y", "0")) - width = parseFloat(node.attr("width")) - height = parseFloat(node.attr("height")) + width = parseFloat(node.attrOrDefault("width", "0")) + height = parseFloat(node.attrOrDefault("height", "0")) + + if width == 0 or height == 0: + return var rx = max(parseFloat(node.attrOrDefault("rx", "0")), 0) @@ -418,9 +466,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = path.rect(x, y, width, height) if ctx.fill != ColorRGBA(): - img.fillPath(path, ctx.fill, ctx.transform) + img.fill(ctx, path) if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) of "circle", "ellipse": let @@ -440,9 +488,9 @@ proc draw(img: ptr Context, node: XmlNode, ctxStack: var seq[Ctx]) = path.ellipse(cx, cy, rx, ry) if ctx.fill != ColorRGBA(): - img.fillPath(path, ctx.fill, ctx.transform) + img.fill(ctx, path) if ctx.shouldStroke: - img.strokePath(path, ctx.stroke, ctx.transform, ctx.strokeWidth) + img.stroke(ctx, path) else: raise newException(PixieError, "Unsupported SVG tag: " & node.tag & ".") @@ -457,12 +505,19 @@ proc decodeSvg*(data: string, width = 0, height = 0): Image = let viewBox = root.attr("viewBox") box = viewBox.split(" ") + viewBoxMinX = parseInt(box[0]) + viewBoxMinY = parseInt(box[1]) viewBoxWidth = parseInt(box[2]) viewBoxHeight = parseInt(box[3]) var rootCtx = initCtx() rootCtx = decodeCtx(rootCtx, root) + if viewBoxMinX != 0 or viewBoxMinY != 0: + rootCtx.transform = rootCtx.transform * translate( + vec2(-viewBoxMinX.float32, -viewBoxMinY.float32) + ) + var surface: ptr Surface if width == 0 and height == 0: # Default to the view box size result = newImage(viewBoxWidth, viewBoxHeight) @@ -476,7 +531,7 @@ proc decodeSvg*(data: string, width = 0, height = 0): Image = let scaleX = width.float32 / viewBoxWidth.float32 scaleY = height.float32 / viewBoxHeight.float32 - rootCtx.transform = scale(vec2(scaleX, scaleY)) + rootCtx.transform = rootCtx.transform * scale(vec2(scaleX, scaleY)) let c = surface.create() diff --git a/experiments/test_svg_cairo.nim b/experiments/test_svg_cairo.nim index 5e54f94..25f96e1 100644 --- a/experiments/test_svg_cairo.nim +++ b/experiments/test_svg_cairo.nim @@ -14,10 +14,5 @@ const files = [ ] for file in files: - let - original = readFile(&"tests/images/svg/{file}.svg") - image = decodeSvg(original) - gold = readImage(&"tests/images/svg/{file}.png") - - # doAssert image.data == gold.data + let image = decodeSvg(readFile(&"tests/images/svg/{file}.svg")) image.writeFile(&"tests/images/svg/{file}.png") diff --git a/pixie.nimble b/pixie.nimble index 17e533c..cbd3ffd 100644 --- a/pixie.nimble +++ b/pixie.nimble @@ -1,4 +1,4 @@ -version = "2.0.0" +version = "2.0.1" author = "Andre von Houck and Ryan Oldenburg" description = "Full-featured 2d graphics library for Nim." license = "MIT" diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index 70ce9fb..b017d21 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -36,6 +36,10 @@ type SomePath* = Path | string + Partitioning = object + partitions: seq[seq[(Segment, int16)]] + startY, partitionHeight: uint32 + const epsilon = 0.0001 * PI ## Tiny value used for some computations. defaultMiterLimit*: float32 = 4 @@ -604,8 +608,8 @@ proc polygon*(path: var Path, pos: Vec2, size: float32, sides: int) {.inline.} = ## Adds a n-sided regular polygon at (x, y) with the parameter size. path.polygon(pos.x, pos.y, size, sides) -proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] = - ## Converts SVG-like commands to line segments. +proc commandsToShapes(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] = + ## Converts SVG-like commands to sequences of vectors. var start, at: Vec2 shape: seq[Vec2] @@ -962,6 +966,90 @@ proc commandsToShapes*(path: Path, pixelScale: float32 = 1.0): seq[seq[Vec2]] = if shape.len > 0: result.add(shape) +proc shapesToSegments(shapes: seq[seq[Vec2]]): seq[(Segment, int16)] = + ## Converts the shapes into a set of filtered segments with winding value. + var segmentCount: int + for shape in shapes: + segmentCount += shape.len - 1 + + for shape in shapes: + for segment in shape.segments: + if segment.at.y == segment.to.y: # Skip horizontal + continue + var + segment = segment + winding = 1.int16 + if segment.at.y > segment.to.y: + swap(segment.at, segment.to) + winding = -1 + + result.add((segment, winding)) + +proc computePixelBounds(segments: seq[(Segment, int16)]): Rect = + ## Compute the bounds of the segments. + var + xMin = float32.high + xMax = float32.low + yMin = float32.high + yMax = float32.low + for (segment, _) in segments: + xMin = min(xMin, min(segment.at.x, segment.to.x)) + xMax = max(xMax, max(segment.at.x, segment.to.x)) + yMin = min(yMin, min(segment.at.y, segment.to.y)) + yMax = max(yMax, max(segment.at.y, segment.to.y)) + + xMin = floor(xMin) + xMax = ceil(xMax) + yMin = floor(yMin) + yMax = ceil(yMax) + + if xMin.isNaN() or xMax.isNaN() or yMin.isNaN() or yMax.isNaN(): + discard + else: + result.x = xMin + result.y = yMin + result.w = xMax - xMin + result.h = yMax - yMin + +proc computePixelBounds*(path: Path): Rect = + ## Compute the bounds of the path. + path.commandsToShapes().shapesToSegments().computePixelBounds() + +proc partitionSegments( + segments: seq[(Segment, int16)], top, height: int +): Partitioning = + ## Puts segments into the height partitions they intersect with. + let + maxPartitions = max(1, height div 10).uint32 + numPartitions = min(maxPartitions, max(1, segments.len div 10).uint32) + + result.partitions.setLen(numPartitions) + result.startY = top.uint32 + result.partitionHeight = height.uint32 div numPartitions + + for (segment, winding) in segments: + if result.partitionHeight == 0: + result.partitions[0].add((segment, winding)) + else: + var + atPartition = max(0, segment.at.y - result.startY.float32).uint32 + toPartition = max(0, ceil(segment.to.y - result.startY.float32)).uint32 + atPartition = atPartition div result.partitionHeight + toPartition = toPartition div result.partitionHeight + atPartition = clamp(atPartition, 0, result.partitions.high.uint32) + toPartition = clamp(toPartition, 0, result.partitions.high.uint32) + for i in atPartition .. toPartition: + result.partitions[i].add((segment, winding)) + +proc getIndexForY(partitioning: Partitioning, y: int): uint32 {.inline.} = + if partitioning.partitionHeight == 0 or partitioning.partitions.len == 1: + 0.uint32 + else: + min( + (y.uint32 - partitioning.startY) div partitioning.partitionHeight, + partitioning.partitions.high.uint32 + ) + proc quickSort(a: var seq[(float32, int16)], inl, inr: int) = ## Sorts in place + faster than standard lib sort. var @@ -983,30 +1071,6 @@ proc quickSort(a: var seq[(float32, int16)], inl, inr: int) = quickSort(a, inl, r) quickSort(a, l, inr) -proc computeBounds(partitions: seq[seq[(Segment, int16)]]): Rect = - ## Compute bounds of a shape segments with windings. - var - xMin = float32.high - xMax = float32.low - yMin = float32.high - yMax = float32.low - for partition in partitions: - for (segment, _) in partition: - xMin = min(xMin, min(segment.at.x, segment.to.x)) - xMax = max(xMax, max(segment.at.x, segment.to.x)) - yMin = min(yMin, min(segment.at.y, segment.to.y)) - yMax = max(yMax, max(segment.at.y, segment.to.y)) - - xMin = floor(xMin) - xMax = ceil(xMax) - yMin = floor(yMin) - yMax = ceil(yMax) - - result.x = xMin - result.y = yMin - result.w = xMax - xMin - result.h = yMax - yMin - proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} = ## Should we fill based on the current winding rule and count? case windingRule: @@ -1015,49 +1079,12 @@ proc shouldFill(windingRule: WindingRule, count: int): bool {.inline.} = of wrEvenOdd: count mod 2 != 0 -proc partitionSegments( - shapes: seq[seq[Vec2]], height: int -): seq[seq[(Segment, int16)]] = - ## Puts segments into the height partitions they intersect with. - var segmentCount: int - for shape in shapes: - segmentCount += shape.len - 1 - - let - maxPartitions = max(1, height div 10).uint32 - numPartitions = min(maxPartitions, max(1, segmentCount div 10).uint32) - partitionHeight = (height.uint32 div numPartitions) - - result.setLen(numPartitions) - for shape in shapes: - for segment in shape.segments: - if segment.at.y == segment.to.y: # Skip horizontal - continue - var - segment = segment - winding = 1.int16 - if segment.at.y > segment.to.y: - swap(segment.at, segment.to) - winding = -1 - - if partitionHeight == 0: - result[0].add((segment, winding)) - else: - var - atPartition = max(0, segment.at.y).uint32 div partitionHeight - toPartition = max(0, ceil(segment.to.y)).uint32 div partitionHeight - atPartition = clamp(atPartition, 0, result.high.uint32) - toPartition = clamp(toPartition, 0, result.high.uint32) - for i in atPartition .. toPartition: - result[i].add((segment, winding)) - template computeCoverages( coverages: var seq[uint8], hits: var seq[(float32, int16)], size: Vec2, y: int, - partitions: seq[seq[(Segment, int16)]], - partitionHeight: uint32, + partitioning: Partitioning, windingRule: WindingRule ) = const @@ -1066,16 +1093,10 @@ template computeCoverages( offset = 1 / quality.float32 initialOffset = offset / 2 + epsilon - let - partition = - if partitionHeight == 0 or partitions.len == 1: - 0.uint32 - else: - min(y.uint32 div partitionHeight, partitions.high.uint32) - zeroMem(coverages[0].addr, coverages.len) # Do scanlines for this row + let partition = getIndexForY(partitioning, y) var yLine = y.float32 + initialOffset - offset numHits: int @@ -1083,10 +1104,10 @@ template computeCoverages( yLine += offset let scanline = line(vec2(0, yLine), vec2(size.x, yLine)) numHits = 0 - for (segment, winding) in partitions[partition]: + for (segment, winding) in partitioning.partitions[partition]: if segment.at.y <= scanline.a.y and segment.to.y >= scanline.a.y: var at: Vec2 - if scanline.intersects(segment, at): # and segment.to != at: + if scanline.intersects(segment, at) and segment.to != at: if numHits == hits.len: hits.setLen(hits.len * 2) hits[numHits] = (min(at.x, size.x), winding) @@ -1150,19 +1171,18 @@ proc fillShapes( windingRule: WindingRule, blendMode: BlendMode ) = - let - rgbx = color.asRgbx() - partitions = partitionSegments(shapes, image.height) - partitionHeight = image.height.uint32 div partitions.len.uint32 - # Figure out the total bounds of all the shapes, # rasterize only within the total bounds let - bounds = computeBounds(partitions) + rgbx = color.asRgbx() + blender = blendMode.blender() + segments = shapes.shapesToSegments() + bounds = computePixelBounds(segments) startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) stopY = min(image.height, (bounds.y + bounds.h).int) - blender = blendMode.blender() + pathHeight = stopY - startY + partitions = partitionSegments(segments, startY, pathHeight) var coverages = newSeq[uint8](image.width) @@ -1175,7 +1195,6 @@ proc fillShapes( image.wh, y, partitions, - partitionHeight, windingRule ) @@ -1264,17 +1283,16 @@ proc fillShapes( shapes: seq[seq[Vec2]], windingRule: WindingRule ) = - let - partitions = partitionSegments(shapes, mask.height) - partitionHeight = mask.height.uint32 div partitions.len.uint32 - # Figure out the total bounds of all the shapes, # rasterize only within the total bounds let - bounds = computeBounds(partitions) + segments = shapes.shapesToSegments() + bounds = computePixelBounds(segments) startX = max(0, bounds.x.int) startY = max(0, bounds.y.int) stopY = min(mask.height, (bounds.y + bounds.h).int) + pathHeight = stopY - startY + partitions = partitionSegments(segments, startY, pathHeight) when defined(amd64) and not defined(pixieNoSimd): let maskerSimd = bmNormal.maskerSimd() @@ -1290,7 +1308,6 @@ proc fillShapes( mask.wh, y, partitions, - partitionHeight, windingRule ) @@ -1488,7 +1505,7 @@ proc parseSomePath( proc transform(shapes: var seq[seq[Vec2]], transform: Vec2 | Mat3) = when type(transform) is Vec2: - if transform != vec2(0, 0): + if transform != vec2(): for shape in shapes.mitems: for segment in shape.mitems: segment += transform @@ -1590,8 +1607,7 @@ proc strokePath*( dashes ) strokeShapes.transform(transform) - image.fillShapes( - strokeShapes, paint.color, wrNonZero, blendMode = paint.blendMode) + image.fillShapes(strokeShapes, paint.color, wrNonZero, paint.blendMode) return let diff --git a/tests/images/svg/emojitwo.png b/tests/images/svg/emojitwo.png new file mode 100644 index 0000000..9f619a8 Binary files /dev/null and b/tests/images/svg/emojitwo.png differ diff --git a/tests/images/svg/noto-emoji.png b/tests/images/svg/noto-emoji.png new file mode 100644 index 0000000..b74b627 Binary files /dev/null and b/tests/images/svg/noto-emoji.png differ diff --git a/tests/images/svg/openmoji.png b/tests/images/svg/openmoji.png index 1b2931a..bfe29ec 100644 Binary files a/tests/images/svg/openmoji.png and b/tests/images/svg/openmoji.png differ diff --git a/tests/emoji_megatest.nim b/tests/megatest_emoji.nim similarity index 78% rename from tests/emoji_megatest.nim rename to tests/megatest_emoji.nim index 0807ccc..554e551 100644 --- a/tests/emoji_megatest.nim +++ b/tests/megatest_emoji.nim @@ -6,6 +6,12 @@ import cligen, os, pixie, pixie/fileformats/svg, strformat # Clone https://github.com/hfg-gmuend/openmoji # Check out commit c1f14ae0be29b20c7eed215d1e03df23b1c9a5d5 +# Clone https://github.com/EmojiTwo/emojitwo +# Check out commit d79b4477eb8f9110fc3ce7bed2cc66030a77933e + +# Clone https://github.com/googlefonts/noto-emoji +# Check out commit 948b1a7f1ed4ec7e27930ad8e027a740db3fe25e + type EmojiSet = object name: string path: string @@ -13,7 +19,9 @@ type EmojiSet = object const emojiSets = [ EmojiSet(name: "twemoji", path: "../twemoji/assets/svg/*"), - EmojiSet(name: "openmoji", path: "../openmoji/color/svg/*") + EmojiSet(name: "openmoji", path: "../openmoji/color/svg/*"), + EmojiSet(name: "emojitwo", path: "../emojitwo/svg/*"), + EmojiSet(name: "noto-emoji", path: "../noto-emoji/svg/*") ] width = 32 height = 32 diff --git a/tests/fonts_megatest.nim b/tests/megatest_fonts.nim similarity index 100% rename from tests/fonts_megatest.nim rename to tests/megatest_fonts.nim diff --git a/tests/icons_megatest.nim b/tests/megatest_icons.nim similarity index 100% rename from tests/icons_megatest.nim rename to tests/megatest_icons.nim