Given a 2D grayscale image image as a list of lists of integers and a square filter kernel as a list of lists of integers, implement a function that returns a new 2D image produced by applying the kernel to every pixel using convolution-like neighborhood summation. Assume zero-padding outside image boundaries, and clamp each output value to the range [0, 255].
def apply_filter(image, kernel):