ScreenshotNeo

BlogHow-to

How to Fix Images Not Rendering in IMGKit JPG Conversion

Fix missing images in IMGKit JPG output by checking paths, local-file access, JavaScript timing, renderer options, permissions, and wkhtmltoimage.

By the ScreenshotNeo team1 October 20268 min read

When images are missing from an IMGKit JPG, the JPEG encoder is usually not the problem. IMGKit sends HTML to wkhtmltoimage; the renderer must first find and load each image, then encode the rendered page. Check the image URL or file path, local-file permissions, authentication, JavaScript timing, IMGKit options, and the wkhtmltoimage binary in that order.

Quick diagnostic checklist

  1. Inspect the exact HTML passed to IMGKit and confirm every <img src> exists.
  2. Replace relative image URLs with absolute HTTPS URLs, or use a correct local file URL.
  3. Test the URL or file from the same host, container, user, and working directory that runs wkhtmltoimage.
  4. For local files, enable local-file access and allow only the directory containing the assets.
  5. Remove any accidental no-images option.
  6. If JavaScript creates or changes the image, enable JavaScript and add a suitable delay.
  7. Run the equivalent wkhtmltoimage command directly and read stderr.
  8. Confirm IMGKit points to a compatible executable.
  9. Render PNG or a high-quality JPG while diagnosing; JPEG quality cannot restore an image that was never loaded.
  10. Flush temporary files before another process reads or uploads them.

How IMGKit image loading works

IMGKit is a Ruby wrapper around wkhtmltoimage. It accepts HTML, a URL, or a file. Relative URLs therefore resolve from the renderer’s input context, which may differ from the context in your browser or Rails development server. A browser that displays an image successfully does not prove that the conversion process can reach it.

There are two separate stages:

  1. Resource loading: wkhtmltoimage resolves URLs, reads local files, performs network requests, executes JavaScript, and paints the page.
  2. Encoding: the painted result is written as PNG, JPG, or another supported output. The quality option affects JPEG compression only.

Step 1: Inspect the generated HTML and resolve URLs

Log or save the exact HTML string sent to IMGKit. Look for empty src values, incorrect capitalization, Rails asset fingerprints, protocol-relative URLs, and paths that only work inside a browser.

html = render_to_string(template: "cards/show", formats: [:html])
File.write("/tmp/imgkit-input.html", html)
puts html[/<img\b[^>]*>/i]

Prefer an absolute URL for a network image:

<img src="https://static.example.com/images/logo.png" alt="Logo">

For a local file, verify the path exists inside the process that invokes wkhtmltoimage. A path on your laptop is not available inside a container unless it is mounted there.

path = Rails.root.join("public", "images", "logo.png")
abort "Missing #{path}" unless File.file?(path)
puts path.to_s

Check filename case. Linux treats Logo.PNG and logo.png as different files even when a development machine does not.

Step 2: Test reachability from the conversion environment

Run checks as the same operating-system user and from the same container or host that performs the conversion.

# Network asset
curl -I -L --max-time 20 https://static.example.com/images/logo.png

# Local asset
stat /app/public/images/logo.png
namei -l /app/public/images/logo.png

Investigate redirects, DNS, TLS certificates, firewall rules, HTTP authentication, signed URL expiry, and permissions. A successful request in your browser may use cookies, a VPN, or credentials unavailable to wkhtmltoimage.

Step 3: Allow local images safely

wkhtmltoimage can block local-file access. Its command-line controls include --enable-local-file-access and repeatable --allow directory permissions. Permit only the asset directory required by the render.

wkhtmltoimage \
  --enable-local-file-access \
  --allow /app/public/images \
  /app/tmp/input.html /app/tmp/output.jpg

In IMGKit, pass the corresponding options. Option spelling depends on the installed build, so confirm with wkhtmltoimage --extended-help.

IMGKit.configure do |config|
  config.wkhtmltoimage = "/absolute/path/to/wkhtmltoimage"
  config.default_options = {
    "enable-local-file-access" => true,
    "allow" => [Rails.root.join("public", "images").to_s]
  }
end

html = render_to_string(template: "cards/show", formats: [:html])
kit = IMGKit.new(html, "enable-local-file-access" => true)
File.binwrite("out.jpg", kit.to_jpg)

Do not allow the entire filesystem when a single asset directory is sufficient. Broad permissions make accidental or unintended file reads easier.

Step 4: Make sure images are enabled

IMGKit passes wkhtmltoimage options through. An inherited default such as no-images disables image loading even though the HTML is correct.

IMGKit.configure do |config|
  config.default_options = {
    # Do not set "no-images" => true
    "enable-local-file-access" => true
  }
end

Inspect shared configuration, environment-specific initializers, job arguments, and wrapper methods. If a command is assembled dynamically, print the final options before conversion.

Step 5: Wait for JavaScript-generated images

Images may be inserted after page load, have their src assigned by JavaScript, or receive a signed URL asynchronously. Enable JavaScript and wait long enough for the image element and its resource to appear.

options = {
  "enable-javascript" => true,
  "javascript-delay" => 1000,
  "debug-javascript" => true
}

kit = IMGKit.new(html, options)
File.binwrite("out.jpg", kit.to_jpg)

A delay starts after page load; it is not a guarantee that every image has finished downloading. For deterministic pages, render a server-generated src and avoid unnecessary client-side work. Increase the delay only after confirming that timing is the cause.

Step 6: Reproduce with wkhtmltoimage directly

IMGKit can hide the underlying renderer command and stderr. Save the input HTML and run the binary directly with verbose output.

wkhtmltoimage --extended-help | less
wkhtmltoimage --log-level info --enable-local-file-access \
  --allow /app/public/images \
  /tmp/imgkit-input.html /tmp/direct.jpg

Look for messages about missing files, HTTP failures, TLS, permissions, unsupported resources, or JavaScript errors. If the direct command fails, fix the renderer inputs or options before changing Ruby code. If it succeeds, compare its options and working directory with IMGKit’s invocation.

Step 7: Verify the wkhtmltoimage executable

A missing or incompatible binary can produce blank output or fail before resources are loaded. Set an absolute path when the executable is not on PATH or deployment uses a platform-specific build.

IMGKit.configure do |config|
  config.wkhtmltoimage = "/opt/wkhtmltox/bin/wkhtmltoimage"
end

puts `#{IMGKit.configuration.wkhtmltoimage} --version`

Use the binary’s own --extended-help to verify that options such as local-file access, image loading, JavaScript delay, and logging exist in that build.

Complete minimal Ruby example

require "imgkit"

IMGKit.configure do |config|
  config.wkhtmltoimage = "/absolute/path/to/wkhtmltoimage"
  config.default_options = {
    "enable-local-file-access" => true,
    "allow" => ["/app/public/images"],
    "enable-javascript" => true,
    "javascript-delay" => 500,
    "log-level" => "info"
  }
end

html = <<~HTML
  <!doctype html>
  <html>
    <body>
      <img src="file:///app/public/images/logo.png" width="240" alt="Logo">
    </body>
  </html>
HTML

kit = IMGKit.new(html)
File.binwrite("out.jpg", kit.to_jpg)

Use a file URL only when it matches the path visible to the renderer. If your build requires a different spelling, follow its extended help output.

Output handling and JPEG quality

First produce a PNG or a high-quality JPG to separate loading problems from encoding artifacts.

kit = IMGKit.new(html, "quality" => 95)
File.binwrite("diagnostic.png", kit.to_png)
File.binwrite("diagnostic.jpg", kit.to_jpg)

JPEG quality changes compression after rendering. It cannot bring back an image that was blocked, timed out, or never inserted. When writing to a Ruby Tempfile or another buffered IO object, call flush before another process reads or uploads it.

require "tempfile"

file = Tempfile.new(["capture", ".jpg"])
file.binmode
file.write(kit.to_jpg)
file.flush
# Upload or read file.path only after flush.

Common errors and fixes

Symptom Likely cause Fix
All images are missing no-images is enabled or the renderer cannot access resources Remove the option, test direct wkhtmltoimage, and inspect stderr.
Only relative URLs fail They resolve from an unexpected HTML location Use absolute HTTPS URLs or a correct file URL and verify the input context.
Local files fail with a warning Local-file access is blocked Enable it and add a narrow --allow directory.
Images work in a browser but not production Different user, container, DNS, credentials, or mounted paths Run curl/stat as the conversion user in the production environment.
Image appears intermittently JavaScript or a slow network request finishes after capture Use a suitable javascript-delay, simplify the page, or make the URL available before rendering.
Private image returns 401 or 403 wkhtmltoimage has no browser cookies or authorization Use a reachable signed URL or configure the renderer’s supported headers/cookies.
Output is blank Page load, binary, permission, or unsupported-resource failure Reduce to one HTML image, run the binary directly, and read diagnostics.
Output file is incomplete Ruby IO buffering Flush the IO before reading or uploading it.
Option has no effect Installed wkhtmltoimage build uses different support or spelling Check wkhtmltoimage --extended-help and the exact binary path.

Reduce the case when the page is still blank

  1. Create a document with one local PNG and one absolute HTTPS image.
  2. Remove CSS background images, lazy loading, authentication, redirects, and JavaScript.
  3. Render that HTML directly with wkhtmltoimage.
  4. Add dependencies back one at a time: local access, redirects, CSS, JavaScript, then authentication.

This isolates whether the failure is path resolution, local-file policy, network access, timing, or an unsupported renderer feature.

Performance, reliability, and cost considerations

  • Keep inputs small: large pages, many images, and long JavaScript delays increase conversion time and memory use.
  • Prefer deterministic assets: stable absolute URLs and pre-generated HTML are easier to retry than client-side image assembly.
  • Set timeouts: bound the worker job and network requests so one unreachable image does not hold a worker indefinitely.
  • Retry selectively: retry transient network failures, but fix persistent 404, 401, 403, path, and permission errors instead of repeating them.
  • Pin the renderer: use the same wkhtmltoimage build in development, CI, and production and record its version.
  • Measure the right stage: log render duration, output size, and stderr separately from application upload time.
  • Limit local access: narrow allow lists improve reliability and reduce accidental file exposure.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API when you do not want to maintain a wkhtmltoimage binary and its resource-loading rules. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and each response identifies the page verdict and billing status in headers. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for all options. A one-call capture looks like this:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Does changing JPG quality fix missing images?

No. Quality controls compression after rendering; it cannot load an image that failed.

Should I use a local path or a URL?

Use whichever the renderer can reach reliably. Absolute HTTPS URLs are usually simplest; local paths require correct visibility and permissions inside the conversion environment.

Why does a browser show the image while IMGKit does not?

The browser may have cookies, credentials, a different working directory, JavaScript time, or network access that wkhtmltoimage lacks.

How can I tell whether the failure is IMGKit or wkhtmltoimage?

Save the HTML and run the equivalent wkhtmltoimage command directly. Compare its output and stderr with the IMGKit result.

Is local-file access required for every image?

No. It is relevant to local files. Public network images use network access, while private network images still require credentials or a reachable signed URL.