Системы управления документами на основе веб-технологий быстро развиваются, и часто востребованной важной функцией является исправление документов. Dynamsoft Document Normalizer SDK предлагает набор удобных API-интерфейсов JavaScript для обнаружения и исправления документов в веб-браузерах. Эта статья проведет вас через процесс создания веб-приложения для исправления документов.

Попробуйте онлайн-демо

https://yushulx.me/dotnet-blazor-document-rectification/

Предварительные условия

  • .NET SDK
  • Получите версию JavaScript Dynamsoft Document Normalizer SDK от NPM или загрузите ее непосредственно с cdn.jsdelivr.net:
  • Запросите пробную лицензию.

Шаг 1. Настройка проекта Blazor WebAssembly

  1. Создайте проект Blazor WebAssembly с помощью следующей команды:
dotnet new blazorwasm -o BlazorDocRectifySample

2. Перейдите в новый каталог.

 cd BlazorDocRectifySample

3. Откройте wwwroot/index.html и добавьте Dynamsoft Camera Enhancer SDK и Dynamsoft Document Normalizer SDK:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/dce.js"></script>
 <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ddn.js"></script>
  • Dynamsoft Camera Enhancer — это инструмент, используемый для доступа к видеопотоку веб-камеры. Он доступен бесплатно.
  • Dynamsoft Document Normalizer SDK используется для исправления документов. Для его использования требуется действующий лицензионный ключ.

4. Создайте файл jsInterop.js для взаимодействия между C# и JavaScript, а затем добавьте его в файл index.html:

Шаг 2. Создание пользовательского интерфейса

Мы создаем два компонента Razor: один для обнаружения документов из файла изображения, а другой — для обнаружения документов из потока камеры.

dotnet new razorcomponent -n ImageFile -o Pages
dotnet new razorcomponent -n CameraStream -o Pages

Компонент ImageFile

@page "/image-file"
@inject IJSRuntime JSRuntime

<div>
<button class="btn" @onclick="DocDetect">Load file</button>
<button class="btn" @onclick="Rectify">Rectify</button>
<button class="btn" @onclick="Save">Save</button>
</div>

<div>
    <input type="radio" name="format" value="grayscale" @onchange="HandleInputChange">Grayscale
    <input type="radio" name="format" value="color" checked @onchange="HandleInputChange">Color
    <input type="radio" name="format" value="binary" @onchange="HandleInputChange">Binary
</div>

<div class="container">
    <div>
        <div id="imageview">
            <img id="image" />
            <canvas id="overlay"></canvas>
        </div>
    </div>
    <div>
        <canvas id="canvas"></canvas>
    </div>
</div>

  • Метод DocDetect() вызывается при нажатии кнопки. Он вызывает метод selectFile() в JavaScript и передает ему ссылку на объект C# и идентификаторы холста. При обнаружении документа одно полотно используется для отображения обнаруженного четырехугольника, а другое — для отображения исправленного документа.
public async Task DocDetect()
  {
      await JSRuntime.InvokeVoidAsync(
      "jsFunctions.selectFile", objRef, "image");
  }
  • Метод Rectify() запускает исправление документа вручную.
public async Task Rectify()
  {
      await JSRuntime.InvokeVoidAsync(
      "jsFunctions.rectify");
  }
  • Метод Save() сохраняет исправленный документ в файл изображения.
public async Task Save()
  {
      await JSRuntime.InvokeVoidAsync(
      "jsFunctions.save", objRef, "canvas");
  }
  • Метод HandleInputChange() обрабатывает событие изменения переключателя. Он предоставляет три варианта выходного формата цвета: оттенки серого, цветной и двоичный.
public async Task HandleInputChange(ChangeEventArgs e)
  {
      await JSRuntime.InvokeVoidAsync(
      "jsFunctions.setOutputFormat", e.Value.ToString());
  }
  • Элемент <img> используется для отображения выбранного файла изображения.

Компонент CameraStream

@page "/camera-stream"
@inject IJSRuntime JSRuntime

<div class="select">
    <label for="videoSource">Video source: </label>
    <select id="videoSource"></select>
</div>

<div>
<button class="btn" @onclick="Rectify">Rectify</button>
<button class="btn" @onclick="Save">Save</button>
</div>

<div class="container">
    <div id="videoview">
        <div class="dce-video-container" id="videoContainer"></div>
        <canvas id="overlay"></canvas>
    </div>
    <div>
        <canvas id="canvas"></canvas>
    </div>
</div>

  • Элемент <select> перечисляет все доступные камеры.
  • Кнопки и холсты работают так же, как и в компоненте ImageFile.
  • Поток камеры отображается в элементе <div> с классом dce-video-container.

Шаг 3. Реализация логики исправления документов

Основная логика исправления документов реализована в JavaScript. Основные методы взаимодействия определены в файле wwwroot/jsInterop.js:

window.jsFunctions = {
    initSDK: async function (licenseKey) {
        
    },
    initImageFile: async function (dotnetRef, canvasId) {
        
    },
    initScanner: async function (dotnetRef, videoId, selectId, canvasOverlayId, canvasId) {
        
    },
    selectFile: async function (dotnetRef, canvasOverlayId, imageId) {
        
    },
    rectify: async function () {
        
    },
    updateSetting: async function (color) {
        
    },
    save: async function () {
        
    },
};
  • Метод initSDK() инициализирует Dynamsoft Document Normalizer SDK действительным лицензионным ключом. Он также устанавливает настройки времени выполнения для исправления документов.
async function init() {
      normalizer = await Dynamsoft.DDN.DocumentNormalizer.createInstance();
      let settings = await normalizer.getRuntimeSettings();
      settings.ImageParameterArray[0].BinarizationModes[0].ThresholdCompensation = 9;
      settings.NormalizerParameterArray[0].ColourMode = "ICM_COLOUR"; // ICM_BINARY, ICM_GRAYSCALE, ICM_COLOUR
    
      await normalizer.setRuntimeSettings(settings);
  }
    
  initSDK: async function (licenseKey) {
      let result = true;

      if (normalizer != null) {
          return result;
      }
  • Метод initImageFile() инициализирует холсты для отрисовки четырехугольника и исправленного документа.
initImageFile: async function (dotnetRef, canvasOverlayId, canvasRectifyId) {
      dotnetHelper = dotnetRef;
      initOverlay(document.getElementById(canvasOverlayId));
      canvasRectify = document.getElementById(canvasRectifyId);
      contextRectify = canvasRectify.getContext('2d');
      if (normalizer != null) {
          normalizer.stopScanning();
      }
      await init();

      return true;
  }
  • Метод initCameraStream() инициализирует поток камеры и холсты для рендеринга четырехугольника и исправленного документа.
initCameraStream: async function (dotnetRef, videoId, selectId, canvasOverlayId, canvasId) {
      await init();
      canvasRectify = document.getElementById(canvasId);
      contextRectify = canvasRectify.getContext('2d');
      let canvas = document.getElementById(canvasOverlayId);
      data = {};
      initOverlay(canvas);
      videoContainer = document.getElementById(videoId);
      videoSelect = document.getElementById(selectId);
      videoSelect.onchange = openCamera;
      dotnetHelper = dotnetRef;

      try {
          enhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance();
          await enhancer.setUIElement(document.getElementById(videoId));
          await normalizer.setImageSource(enhancer, { });
          await normalizer.startScanning(true);
          let cameras = await enhancer.getAllCameras();
          listCameras(cameras);
          await openCamera();

          normalizer.onQuadDetected = (quads, sourceImage) => {
              clearOverlay();
              if (quads.length == 0) {
                  return;
              }
              data["file"] = sourceImage;
              let location = quads[0].location;
              data["points"] = quads[0].location;
              drawQuad(location.points);
          };
          enhancer.on("played", playCallBackInfo => {
              updateResolution();
          });

      } catch (e) {
          console.log(e);
          result = false;
      }
      return true;
  }
  • Метод selectFile() обнаруживает документы из файла изображения.
selectFile: async function (dotnetRef, imageId) {
      data = {};
        
      if (normalizer) {
          let input = document.createElement("input");
          input.type = "file";
          input.onchange = async function () {
              try {
                  let file = input.files[0];
                  var fr = new FileReader();
                  fr.onload = function () {
                      let image = document.getElementById(imageId);
                      image.src = fr.result;
                      image.style.display = 'block';
                        
                      decodeImage(fr.result);
                  }
                  fr.readAsDataURL(file);

              } catch (ex) {
                  alert(ex.message);
                  throw ex;
              }
          };
          input.click();
      } else {
          alert("The SDK is still initializing.");
      }
  }
  • Метод rectify() запускает исправление документа на основе обнаруженного четырехугольника.
async function normalize(file, location) {
      if (file == null || location == null) {
          return;
      }
      if (normalizer) {
          normalizedImageResult = await normalizer.normalize(file, {
              quad: location
          });
          if (normalizedImageResult) {
              let image = normalizedImageResult.image;
              canvasRectify.width = image.width;
              canvasRectify.height = image.height;
              let data = new ImageData(new Uint8ClampedArray(image.data), image.width, image.height);
              contextRectify.clearRect(0, 0, canvasRectify.width, canvasRectify.height);
              contextRectify.putImageData(data, 0, 0);
          }
      }
  }

  rectify: async function () {
      await normalize(data["file"], data["points"]);
  },
  • Метод updateSetting() изменяет цветовой формат исправленного документа.
updateSetting: async function (color) {
      let colorMode = "ICM_GRAYSCALE";
      if (color === 'grayscale') {
          colorMode = "ICM_GRAYSCALE";
      } else if (color === 'color') {
          colorMode = "ICM_COLOUR";
      } else if (color === 'binary') {
          colorMode = "ICM_BINARY";
      }

      if (normalizer && data['file']) {
          let settings = await normalizer.getRuntimeSettings();
          settings.NormalizerParameterArray[0].ColourMode = colorMode;
          await normalizer.setRuntimeSettings(settings);
          normalize(data["file"], data["points"]);
      }
  }
  • Метод save() сохраняет исправленный документ в файл изображения.
save: async function () {
      if (normalizedImageResult) {
          await normalizedImageResult.saveToFile("document-normalization.png", true);
      }
  }

Шаг 4: Редактирование четырехугольника для лучшего исправления

Неизбежно, что обнаруженный четырехугольник может быть неточным. Для улучшения результата ректификации необходима функция четырехстороннего редактирования.

Мы добавляем на холст обработчики событий мыши, чтобы обновлять точки четырехугольника и перерисовывать его.

function initOverlay(ol) {
    canvasOverlay = ol;
    canvasOverlay.addEventListener("mousedown", updatePoint);
    canvasOverlay.addEventListener("touchstart", updatePoint);
    contextOverlay = canvasOverlay.getContext('2d');
}

function updatePoint(e) {
    let points = data["points"].points;
    let rect = canvasOverlay.getBoundingClientRect();

    let scaleX = canvasOverlay.clientWidth / canvasOverlay.width;
    let scaleY = canvasOverlay.clientHeight / canvasOverlay.height;
    let mouseX = (e.clientX - rect.left) / scaleX;
    let mouseY = (e.clientY - rect.top) / scaleY;

    let delta = 10;
    for (let i = 0; i < points.length; i++) {
        if (Math.abs(points[i].x - mouseX) < delta && Math.abs(points[i].y - mouseY) < delta) {
            canvasOverlay.addEventListener("mousemove", dragPoint);
            canvasOverlay.addEventListener("mouseup", releasePoint);
            canvasOverlay.addEventListener("touchmove", dragPoint);
            canvasOverlay.addEventListener("touchend", releasePoint);
            function dragPoint(e) {
                let rect = canvasOverlay.getBoundingClientRect();
                let mouseX = e.clientX || e.touches[0].clientX;
                let mouseY = e.clientY || e.touches[0].clientY;
                points[i].x = Math.round((mouseX - rect.left) / scaleX);
                points[i].y = Math.round((mouseY - rect.top) / scaleY);
                drawQuad(points);
            }
            function releasePoint() {
                canvasOverlay.removeEventListener("mousemove", dragPoint);
                canvasOverlay.removeEventListener("mouseup", releasePoint);
                canvasOverlay.removeEventListener("touchmove", dragPoint);
                canvasOverlay.removeEventListener("touchend", releasePoint);
            }
            break;
        }
    }
}

Исходный код

https://github.com/yushulx/dotnet-blazor-document-rectification

Оригинально опубликовано на сайте https://www.dynamsoft.com 23 августа 2023 г.