index.html 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>OCR</title>
  6. <style>
  7. * {
  8. box-sizing: border-box;
  9. margin: 0;
  10. padding: 0;
  11. }
  12. body, html {
  13. height: 100%;
  14. font-family: sans-serif;
  15. background-color: #ffffff;
  16. }
  17. .container {
  18. display: flex;
  19. flex-direction: column;
  20. align-items: center;
  21. min-height: 100vh;
  22. padding: 5px 20px 20px 20px;
  23. }
  24. .drop-zone {
  25. border: 2px dashed #888;
  26. border-radius: 10px;
  27. width: 400px;
  28. height: 150px;
  29. display: flex;
  30. align-items: center;
  31. justify-content: center;
  32. text-align: center;
  33. color: #555;
  34. font-size: 16px;
  35. cursor: pointer;
  36. transition: background-color 0.3s;
  37. margin-bottom: 20px;
  38. }
  39. .drop-zone.dragover {
  40. background-color: #f0f0f0;
  41. }
  42. .canvas-container {
  43. display: none;
  44. gap: 20px;
  45. flex-wrap: wrap;
  46. justify-content: center;
  47. margin-top: 20px;
  48. }
  49. canvas {
  50. border: 1px solid #ccc;
  51. max-width: 600px;
  52. height: auto;
  53. }
  54. /* 加载动画 */
  55. .loading-overlay {
  56. position: fixed;
  57. top: 0;
  58. left: 0;
  59. right: 0;
  60. bottom: 0;
  61. background: rgba(255, 255, 255, 0.7);
  62. display: flex;
  63. align-items: center;
  64. justify-content: center;
  65. z-index: 9999;
  66. display: none;
  67. }
  68. .spinner {
  69. width: 50px;
  70. height: 50px;
  71. border: 5px solid #ccc;
  72. border-top: 5px solid #3498db;
  73. border-radius: 50%;
  74. animation: spin 1s linear infinite;
  75. }
  76. @keyframes spin {
  77. 0% { transform: rotate(0deg); }
  78. 100% { transform: rotate(360deg); }
  79. }
  80. </style>
  81. </head>
  82. <body>
  83. <div class="container">
  84. <h2>OCR</h2>
  85. <div id="dropZone" class="drop-zone">
  86. 点击或拖动图片到这里上传
  87. </div>
  88. <!-- 加载动画 -->
  89. <div id="loadingOverlay" class="loading-overlay">
  90. <div class="spinner"></div>
  91. </div>
  92. <!-- 结果区域 -->
  93. <div id="canvasContainer" class="canvas-container">
  94. <canvas id="canvasOriginal"></canvas>
  95. <canvas id="canvasTextOnly"></canvas>
  96. </div>
  97. </div>
  98. <script>
  99. const dropZone = document.getElementById('dropZone');
  100. const fileInput = document.createElement('input');
  101. fileInput.type = 'file';
  102. fileInput.accept = 'image/*';
  103. const MAX_DISPLAY_WIDTH = 500;
  104. const canvasOriginal = document.getElementById('canvasOriginal');
  105. const ctxOriginal = canvasOriginal.getContext('2d');
  106. const canvasTextOnly = document.getElementById('canvasTextOnly');
  107. const ctxTextOnly = canvasTextOnly.getContext('2d');
  108. const canvasContainer = document.getElementById('canvasContainer');
  109. const loadingOverlay = document.getElementById('loadingOverlay');
  110. // 点击上传
  111. dropZone.addEventListener('click', () => {
  112. fileInput.click();
  113. });
  114. // 拖拽上传
  115. dropZone.addEventListener('dragover', (e) => {
  116. e.preventDefault();
  117. dropZone.classList.add('dragover');
  118. });
  119. dropZone.addEventListener('dragleave', () => {
  120. dropZone.classList.remove('dragover');
  121. });
  122. dropZone.addEventListener('drop', (e) => {
  123. e.preventDefault();
  124. dropZone.classList.remove('dragover');
  125. const file = e.dataTransfer.files[0];
  126. if (file && file.type.startsWith('image/')) {
  127. handleImage(file);
  128. } else {
  129. alert("请上传图片文件");
  130. }
  131. });
  132. fileInput.addEventListener('change', () => {
  133. const file = fileInput.files[0];
  134. if (file) {
  135. handleImage(file);
  136. }
  137. });
  138. function handleImage(file) {
  139. const reader = new FileReader();
  140. reader.onload = function (e) {
  141. const img = new Image();
  142. img.onload = function () {
  143. const scale = MAX_DISPLAY_WIDTH / img.width;
  144. const displayWidth = img.width * scale;
  145. const displayHeight = img.height * scale;
  146. // 设置 canvas 尺寸
  147. canvasOriginal.width = displayWidth;
  148. canvasOriginal.height = displayHeight;
  149. canvasTextOnly.width = displayWidth;
  150. canvasTextOnly.height = displayHeight;
  151. // 清除之前的绘图
  152. ctxOriginal.clearRect(0, 0, displayWidth, displayHeight);
  153. ctxTextOnly.clearRect(0, 0, displayWidth, displayHeight);
  154. // 绘制原始图像
  155. ctxOriginal.drawImage(img, 0, 0, displayWidth, displayHeight);
  156. // 显示 canvas 容器
  157. canvasContainer.style.display = "flex";
  158. // 显示加载动画
  159. loadingOverlay.style.display = "flex";
  160. const base64Image = e.target.result.split(',')[1];
  161. sendToOCR(base64Image, img.width, img.height, displayWidth, displayHeight);
  162. };
  163. img.src = e.target.result;
  164. };
  165. reader.readAsDataURL(file);
  166. }
  167. function sendToOCR(base64Image, originalWidth, originalHeight, displayWidth, displayHeight) {
  168. fetch('/ocr', {
  169. method: 'POST',
  170. headers: {
  171. 'Content-Type': 'application/json'
  172. },
  173. body: JSON.stringify({ image: base64Image })
  174. })
  175. .then(res => res.json())
  176. .then(data => {
  177. drawBoxesAndTextWithOrientation(data.results, originalWidth, originalHeight, displayWidth, displayHeight);
  178. // 隐藏加载动画
  179. loadingOverlay.style.display = "none";
  180. })
  181. .catch(err => {
  182. console.error("OCR 调用失败", err);
  183. alert("OCR 识别失败,请查看控制台日志");
  184. loadingOverlay.style.display = "none";
  185. });
  186. }
  187. function drawBoxesAndTextWithOrientation(results, originalWidth, originalHeight, displayWidth, displayHeight) {
  188. ctxTextOnly.clearRect(0, 0, canvasTextOnly.width, canvasTextOnly.height);
  189. ctxTextOnly.font = "12px sans-serif";
  190. ctxTextOnly.fillStyle = "black";
  191. const scaleX = displayWidth / originalWidth;
  192. const scaleY = displayHeight / originalHeight;
  193. results.forEach(result => {
  194. const box = result.bounding_box.map(([x, y]) => [
  195. x * scaleX,
  196. y * scaleY
  197. ]);
  198. const [[x1, y1], [x2, y2], [x3, y3]] = box;
  199. const width = Math.abs(x2 - x1);
  200. const height = Math.abs(y3 - y1);
  201. const decodedText = decodeUnicode(result.text);
  202. // 判断方向:宽 < 高 → 竖排
  203. if (width < height && height > 10) {
  204. drawVerticalText(ctxTextOnly, decodedText, x1, y1, height);
  205. } else {
  206. ctxTextOnly.fillText(decodedText, x1, y1 + height / 2);
  207. }
  208. // 绘制左侧红色框
  209. ctxOriginal.strokeStyle = "red";
  210. ctxOriginal.lineWidth = 1;
  211. ctxOriginal.strokeRect(x1, y1, width, height);
  212. });
  213. }
  214. // 竖排文字绘制函数
  215. function drawVerticalText(ctx, text, x, y, height) {
  216. for (let i = 0; i < text.length; i++) {
  217. ctx.fillText(text[i], x, y + i * 14);
  218. }
  219. }
  220. // Unicode 解码函数
  221. function decodeUnicode(str) {
  222. return str.replace(/\\u([0-9a-fA-F]{4})/g, function (_, hex) {
  223. return String.fromCharCode(parseInt(hex, 16));
  224. });
  225. }
  226. </script>
  227. </body>
  228. </html>