The GNU Image manipulation program has a grain merge layer mode that is great for adding highlights and shadows. A grain merge layer that is gray (rgb 128,128,128) won’t affect the image below. Any darker color will make it darker and anything lighter will make the image lighter.
It is computed component wise as Output.rgb = Image.rgb + (Layer.rgb - 128) * Layer.alpha, clamped to the range 0-255. Alpha value is unaffected: Output.alpha = Image.alpha (i.e. if the image below is transparent, it will remain transparent).