SavyScan SDK 0.5.0-beta.7

Integration guide

Install an exact public npm package, serve one pinned image-processing asset, and decide what your application does with the completed PDF.

01 / Quick start

Package integration

Install an exact public npm package version and copy the pinned OpenCV 4.13.0 asset into a versioned, same-origin public path. SavyScan loads that asset only when scanning begins.

No package credential required. npm can download the public SDK without a GitHub account or token. Application use still requires a domain-bound SavyScan trial or commercial license.

Version boundary. The live site evaluates the approved 0.5.0-beta.7 release bytes. Install that exact beta during qualification.

Trial boundary. The tested Apple and Android devices establish a baseline, but each trial customer should run capture, editing, PDF, and delivery in its own application and target devices before production use.

npm install --save-exact @savyscan/[email protected]
mkdir -p public/vendor/savyscan/opencv/4.13.0
cp node_modules/@savyscan/web-sdk/dist/vendor/opencv/4.13.0/opencv.js \
  public/vendor/savyscan/opencv/4.13.0/opencv.js
import SavyScan from "@savyscan/web-sdk";
import "@savyscan/web-sdk/styles.css";

const launchButton = document.querySelector("#scan-document");
const preview = document.querySelector("#scan-preview");
let previewUrl = "";
let scanner = null;

function handleScannerError(error) {
  console.error("SavyScan:", {
    code: error && error.code ? error.code : "SAVYSCAN_ERROR",
    field: error && error.field ? error.field : null,
    message: error && error.message ? error.message : "Scanning failed.",
  });
}

try {
  scanner = SavyScan.create({
    openCvUrl: "/vendor/savyscan/opencv/4.13.0/opencv.js",
    onComplete({ pdfBlob, pageCount }) {
      if (previewUrl) URL.revokeObjectURL(previewUrl);
      previewUrl = URL.createObjectURL(pdfBlob);
      preview.src = previewUrl;
      preview.hidden = false;
      console.log(`Completed ${pageCount} page(s)`);
    },
    onError: handleScannerError,
  });
} catch (error) {
  handleScannerError(error);
}

launchButton.addEventListener("click", () => {
  if (!scanner) return;
  try {
    scanner.open({
      referenceLabel: "Document",
      returnFocusElement: launchButton,
    });
  } catch (error) {
    handleScannerError(error);
  }
});

window.addEventListener("pagehide", (event) => {
  if (!event.persisted) {
    if (scanner) scanner.destroy();
    if (previewUrl) URL.revokeObjectURL(previewUrl);
  }
});

In 0.5.0-beta.7, code and field are stable structured fields and invalid create/open calls throw synchronously.

Always include an Open or Download fallback. Inline PDF rendering is controlled by the host browser, not SavyScan. Mobile Safari may choose not to render a PDF Blob inside an iframe.

02 / No-build integration

Direct browser bundle

Applications without a JavaScript build system can vendor the browser runtime, stylesheet, and OpenCV asset under immutable versioned paths. Put the scripts after the launch button so the mount target exists.

<link rel="stylesheet"
  href="/vendor/savyscan/0.5.0-beta.7/document-scanner.css">

<button id="scan-document" type="button">Scan document</button>

<script src="/vendor/savyscan/0.5.0-beta.7/document-scanner.js"></script>
<script>
  const launchButton = document.querySelector("#scan-document");
  let scanner = null;

  function handleScannerError(error) {
    console.error("SavyScan:", error.code || "SAVYSCAN_ERROR", error.field || null, error.message);
  }

  try {
    scanner = window.SavyScan.create({
      openCvUrl: "/vendor/opencv/4.13.0/opencv.js",
      onComplete(result) {
        // result.pdfBlob: application/pdf
        // result.previewBlob: first corrected page as image/jpeg
        // result.pageCount: number
        handleCompletedScan(result);
      },
      onError: handleScannerError,
    });
  } catch (error) {
    handleScannerError(error);
  }

  launchButton.addEventListener("click", function () {
    if (!scanner) return;
    try {
      scanner.open({ returnFocusElement: launchButton });
    } catch (error) {
      handleScannerError(error);
    }
  });
</script>

OpenCV is lazy-loaded from openCvUrl. Do not also add an eager OpenCV script tag. A 200 JavaScript response must be returned without an authentication redirect or HTML login page.

03 / Lifecycle

One reusable instance

Create the scanner after document.body exists. The SDK mounts a hidden, SDK-owned interface immediately, then opens it from a user action.

Open

Locks page scrolling, makes background siblings inert, loads OpenCV as needed, and starts camera setup.

Close

Stops camera tracks, clears page state, revokes SDK-owned URLs, and restores focus. The same instance can reopen.

Destroy

Permanently removes listeners and an SDK-created root. A destroyed instance cannot reopen.

The successful callback order is onComplete(result), await any returned promise, then onClose({ completed: true }). If onComplete rejects, the scanner stays open so the user can retry or download the completed file.

Use one active scanner per page. OpenCV readiness is shared globally, so every instance should use the same pinned URL and version.

04 / 0.5.0-beta.7 contract

Fail early, recover deliberately

The 0.5.0 beta source uses one SavyScanError type at both integration boundaries. Invalid create(options) and open(options) calls throw synchronously before UI, camera, or session side effects. Reported OpenCV, camera, processing, and host-callback failures use onError; camera capability errors may arrive before open() returns.

function handleScannerError(error) {
  if (error.code === "CAMERA_PERMISSION_DENIED") {
    showPhotoFallbackHelp();
    return;
  }

  reportIntegrationIssue({
    code: error.code,
    field: error.field,
    recoverable: error.recoverable,
  });
}
Code familyRecommended response
CONFIG_REQUIRED, CONFIG_UNKNOWN_KEY, CONFIG_INVALID_TYPE, CONFIG_INVALID_VALUEFix the named field in the host integration. Do not retry unchanged configuration.
ENVIRONMENT_UNSUPPORTED, MOUNT_TARGET_NOT_FOUND, SCANNER_ROOT_INVALIDCorrect the browser or DOM integration before creating another instance.
OPENCV_LOAD_FAILED, OPENCV_INITIALIZATION_FAILEDVerify the immutable asset path, JavaScript response, CSP, authentication, and cache behavior, then retry.
CAMERA_PERMISSION_DENIED, CAMERA_UNAVAILABLE, CAMERA_START_FAILEDKeep the scanner open and direct the user to the existing-photo picker.
PROCESSING_FAILEDKeep the scanner open, show the message, and let the user adjust the page or retry.
HOST_CALLBACK_FAILEDRepair a synchronous notification-callback throw or an onComplete throw or rejection. Errors thrown by onError itself are intentionally swallowed; a rejected onComplete leaves the completed scan open for retry.
INSTANCE_DESTROYEDCreate a new scanner instance; destruction is permanent.

Branch on code and field, not message text. recoverable: true means the same instance can continue or retry. toJSON() excludes the underlying cause, and SavyScan does not copy configured values or URLs into structured diagnostics.

0.5.0-beta.7 boundary. Structured error codes and fields belong to this exact beta contract. Earlier package versions guarantee only a message-shaped error callback.

05 / Host workflows

Upload and email belong to your app

SavyScan creates files; it does not upload documents or send email. Browsers cannot add an attachment to a mailto: link. To email a scan, upload the PDF Blob to an authenticated endpoint and create the attachment on your server.

const scanner = SavyScan.create({
  openCvUrl: "/vendor/savyscan/opencv/4.13.0/opencv.js",
  async onComplete({ pdfBlob, previewBlob, pageCount }) {
    const body = new FormData();
    body.append(
      "document",
      new File([pdfBlob], "scanned-document.pdf", {
        type: "application/pdf",
      })
    );
    body.append(
      "preview",
      new File([previewBlob], "scanned-document-preview.jpg", {
        type: "image/jpeg",
      })
    );
    body.append("pageCount", String(pageCount));

    const response = await fetch("/api/scans/email", {
      method: "POST",
      credentials: "same-origin",
      body,
    });

    if (!response.ok) {
      throw new Error(`The scan could not be emailed (${response.status}).`);
    }
  },
});

Do not set a Content-Type header when sending FormData; the browser adds the multipart boundary. Validate recipients, authorization, MIME type, size, malware policy, and retention server-side.

06 / Security boundary

Document data stays local until you decide otherwise

SavyScan SDK 0.5.0-beta.7 reads the camera or a selected image, processes pixels in browser memory, creates corrected JPEG pages, and returns a PDF Blob to the host callback. It does not upload document images, persist them in local storage or IndexedDB, or send telemetry, license checks, or analytics. An optional onDiagnostic callback exposes privacy-safe events to host code but performs no network request itself.

Secure context and camera policy

For live-camera access, serve the application over HTTPS (localhost is accepted for development) and request camera access from a user action. The existing-image path can still operate without camera permission. An iframe must receive camera permission through both allow="camera" and the server’s Permissions Policy.

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self'; img-src 'self' blob: data:; frame-src 'self' blob:; connect-src 'self'; object-src 'none'; base-uri 'self'
Permissions-Policy: camera=(self)

Treat OpenCV as executable code

Host the pinned OpenCV 4.13.0 asset on the same origin under an immutable path, restrict script-src to trusted origins, and verify the vendored SHA-256 during deployment. This build creates dynamic JavaScript functions in addition to compiling WebAssembly, so 'unsafe-eval' is the controlling allowance. The example also declares the narrower 'wasm-unsafe-eval' token explicitly, although 'unsafe-eval' already permits WebAssembly compilation. The browser self-test proved that 'wasm-unsafe-eval' alone is insufficient. Published 0.4.0 and current beta source do not apply Subresource Integrity or verify the OpenCV version at runtime.

Diagnostics rule. Forward only the frozen event supplied to onDiagnostic. Its closed schema excludes document contents, image bytes, references, filenames, URLs, recipient addresses, device identifiers, wall-clock timestamps, and arbitrary host metadata.

07 / Troubleshooting

Recoverable by design

The camera does not start

Use HTTPS, launch from a user click, confirm browser and operating-system permission, and close other applications using the camera. In an iframe, add allow="camera" and an explicit Permissions Policy. The existing-image picker remains available.

OpenCV cannot load or initialize

Open openCvUrl directly and confirm a 200 JavaScript response—not a redirect or login page. Check CSP, authentication, immutable asset path, and cache headers. Initialization can wait up to 120 seconds.

Automatic capture keeps waiting

Clean the lens, add even light, fill more of the frame, reduce glare, and hold steady. Turn off automatic capture and use the shutter if needed; manual capture still enforces lighting and sharpness quality.

The picker rejects a file

The photo picker accepts one browser-decodable image/* file up to 20 MiB. PDFs and non-image files are rejected. Browser-undecodable HEIC files may need conversion to JPEG or PNG.

The PDF does not render inline

Offer Open and Download actions. Use previewBlob—the first corrected page as JPEG—for a universal visual fallback, and revoke every object URL when it is replaced or the page exits.

The scanner will not reopen

destroy() is permanent. Create a new instance. Use close() when the scanner should be reused.

08 / Migration

Move to the SavyScan name

Use window.SavyScan and the @savyscan/web-sdk package. The older window.DocumentScannerSDK global remains a deprecated alias through 0.5.0 so existing integrations can move without an immediate breaking change.

The legacy supplied root option is also temporary. New integrations should let SavyScan own its interface and optionally provide mountTarget.

Before adopting the 0.5 beta. Audit configuration spelling and types. Values and unknown keys that 0.4.0 tolerated or ignored will fail synchronously. Legacy root, purchaseOrderNumber, and session maxPages remain accepted.