capture-api.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /**
  2. * chrome extension页面截屏API
  3. *
  4. * @copyright https://github.com/mrcoles/full-page-screen-capture-chrome-extension
  5. * @modify zhaoxianlie
  6. */
  7. module.exports = function (MSG_TYPE) {
  8. let MAX_PRIMARY_DIMENSION = 50000 * 2,
  9. MAX_SECONDARY_DIMENSION = 20000 * 2,
  10. MAX_AREA = MAX_PRIMARY_DIMENSION * MAX_SECONDARY_DIMENSION;
  11. let matches = ['http://*/*', 'https://*/*', 'ftp://*/*', 'file://*/*'],
  12. noMatches = [/^https?:\/\/chrome.google.com\/.*$/];
  13. let listenerFunc;
  14. let capturedData = {};
  15. /**
  16. * URL合法性校验
  17. * @param url
  18. * @returns {boolean}
  19. */
  20. function isValidUrl(url) {
  21. // couldn't find a better way to tell if executeScript
  22. // wouldn't work -- so just testing against known urls
  23. // for now...
  24. let r, i;
  25. for (i = noMatches.length - 1; i >= 0; i--) {
  26. if (noMatches[i].test(url)) {
  27. return false;
  28. }
  29. }
  30. for (i = matches.length - 1; i >= 0; i--) {
  31. r = new RegExp('^' + matches[i].replace(/\*/g, '.*') + '$');
  32. if (r.test(url)) {
  33. return true;
  34. }
  35. }
  36. return false;
  37. }
  38. /**
  39. * 执行 capture
  40. * @param data
  41. * @param screenshots
  42. * @param callback
  43. */
  44. function capture(data, screenshots, callback) {
  45. chrome.tabs.captureVisibleTab(
  46. null, {format: 'png', quality: 100}, function (dataURI) {
  47. if (dataURI) {
  48. let image = new Image();
  49. image.onload = function () {
  50. data.image = {width: image.width, height: image.height};
  51. // given device mode emulation or zooming, we may end up with
  52. // a different sized image than expected, so let's adjust to
  53. // match it!
  54. if (data.windowWidth !== image.width) {
  55. let scale = image.width / data.windowWidth;
  56. data.x *= scale;
  57. data.y *= scale;
  58. data.totalWidth *= scale;
  59. data.totalHeight *= scale;
  60. }
  61. // lazy initialization of screenshot canvases (since we need to wait
  62. // for actual image size)
  63. if (!screenshots.length) {
  64. Array.prototype.push.apply(
  65. screenshots,
  66. _initScreenshots(data.totalWidth, data.totalHeight)
  67. );
  68. }
  69. // draw it on matching screenshot canvases
  70. _filterScreenshots(
  71. data.x, data.y, image.width, image.height, screenshots
  72. ).forEach(function (screenshot) {
  73. screenshot.ctx.drawImage(
  74. image,
  75. data.x - screenshot.left,
  76. data.y - screenshot.top
  77. );
  78. });
  79. callback(data);
  80. };
  81. image.src = dataURI;
  82. }
  83. });
  84. }
  85. /**
  86. * 如果页面超级超级长,需要拆分成多张图片来存储
  87. * @param totalWidth
  88. * @param totalHeight
  89. * @returns {Array}
  90. * @private
  91. */
  92. function _initScreenshots(totalWidth, totalHeight) {
  93. let badSize = (totalHeight > MAX_PRIMARY_DIMENSION ||
  94. totalWidth > MAX_PRIMARY_DIMENSION ||
  95. totalHeight * totalWidth > MAX_AREA),
  96. biggerWidth = totalWidth > totalHeight,
  97. maxWidth = (!badSize ? totalWidth :
  98. (biggerWidth ? MAX_PRIMARY_DIMENSION : MAX_SECONDARY_DIMENSION)),
  99. maxHeight = (!badSize ? totalHeight :
  100. (biggerWidth ? MAX_SECONDARY_DIMENSION : MAX_PRIMARY_DIMENSION)),
  101. numCols = Math.ceil(totalWidth / maxWidth),
  102. numRows = Math.ceil(totalHeight / maxHeight),
  103. row, col, canvas, left, top;
  104. let canvasIndex = 0;
  105. let result = [];
  106. for (row = 0; row < numRows; row++) {
  107. for (col = 0; col < numCols; col++) {
  108. canvas = document.createElement('canvas');
  109. canvas.width = (col === numCols - 1 ? totalWidth % maxWidth || maxWidth :
  110. maxWidth);
  111. canvas.height = (row === numRows - 1 ? totalHeight % maxHeight || maxHeight :
  112. maxHeight);
  113. left = col * maxWidth;
  114. top = row * maxHeight;
  115. result.push({
  116. canvas: canvas,
  117. ctx: canvas.getContext('2d'),
  118. index: canvasIndex,
  119. left: left,
  120. right: left + canvas.width,
  121. top: top,
  122. bottom: top + canvas.height
  123. });
  124. canvasIndex++;
  125. }
  126. }
  127. return result;
  128. }
  129. /**
  130. * 从截屏中筛选有效数据
  131. * @param imgLeft
  132. * @param imgTop
  133. * @param imgWidth
  134. * @param imgHeight
  135. * @param screenshots
  136. * @private
  137. */
  138. function _filterScreenshots(imgLeft, imgTop, imgWidth, imgHeight, screenshots) {
  139. // Filter down the screenshots to ones that match the location
  140. // of the given image.
  141. //
  142. let imgRight = imgLeft + imgWidth,
  143. imgBottom = imgTop + imgHeight;
  144. return screenshots.filter(function (screenshot) {
  145. return (imgLeft < screenshot.right &&
  146. imgRight > screenshot.left &&
  147. imgTop < screenshot.bottom &&
  148. imgBottom > screenshot.top);
  149. });
  150. }
  151. /**
  152. * 获取Blobs数据
  153. * @param screenshots
  154. */
  155. function getBlobs(screenshots) {
  156. return screenshots.map(function (screenshot) {
  157. let dataURI = screenshot.canvas.toDataURL();
  158. // convert base64 to raw binary data held in a string
  159. // doesn't handle URLEncoded DataURIs
  160. let byteString = atob(dataURI.split(',')[1]);
  161. // separate out the mime component
  162. let mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
  163. // write the bytes of the string to an ArrayBuffer
  164. let ab = new ArrayBuffer(byteString.length);
  165. let ia = new Uint8Array(ab);
  166. for (let i = 0; i < byteString.length; i++) {
  167. ia[i] = byteString.charCodeAt(i);
  168. }
  169. // create a blob for writing to a file
  170. return new Blob([ab], {type: mimeString});
  171. });
  172. }
  173. /**
  174. * 将Blob数据存储到本地临时文件
  175. * @param blob
  176. * @param filename
  177. * @param index
  178. * @param callback
  179. * @param errback
  180. */
  181. function saveBlob(blob, filename, index, callback, errback) {
  182. filename = ((filename, index) => {
  183. if (!index) {
  184. return filename;
  185. }
  186. let sp = filename.split('.');
  187. let ext = sp.pop();
  188. return sp.join('.') + '-' + (index + 1) + '.' + ext;
  189. })(filename, index);
  190. function onwriteend() {
  191. // open the file that now contains the blob - calling
  192. // `openPage` again if we had to split up the image
  193. let urlName = ('filesystem:chrome-extension://' +
  194. chrome.i18n.getMessage('@@extension_id') +
  195. '/temporary/' + filename);
  196. callback(urlName);
  197. }
  198. // come up with file-system size with a little buffer
  199. let size = blob.size + (1024 / 2);
  200. // create a blob for writing to a file
  201. let reqFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
  202. reqFileSystem(window.TEMPORARY, size, function (fs) {
  203. fs.root.getFile(filename, {create: true}, function (fileEntry) {
  204. fileEntry.createWriter(function (fileWriter) {
  205. fileWriter.onwriteend = onwriteend;
  206. fileWriter.write(blob);
  207. }, errback); // TODO - standardize error callbacks?
  208. }, errback);
  209. }, errback);
  210. }
  211. /**
  212. * 截屏输出screenshots对象
  213. * @param tab
  214. * @param doneback
  215. * @param errback
  216. * @param progress
  217. */
  218. function captureToOrigin(tab, doneback, errback, progress) {
  219. let screenshots = [],
  220. noop = new Function();
  221. doneback = doneback || noop;
  222. errback = errback || noop;
  223. progress = progress || noop;
  224. if (!isValidUrl(tab.url)) {
  225. errback('invalid url');
  226. }
  227. if (typeof listenerFunc !== 'undefined') {
  228. chrome.runtime.onMessage.removeListener(listenerFunc);
  229. }
  230. listenerFunc = function (request, sender, sendResponse) {
  231. if (request.type === MSG_TYPE.PAGE_CAPTURE_CAPTURE) {
  232. progress(request.complete);
  233. capture(request, screenshots, (data) => {
  234. sendResponse(data);
  235. request.complete === 1 && doneback(screenshots);
  236. });
  237. return true;
  238. }
  239. };
  240. chrome.runtime.onMessage.addListener(listenerFunc);
  241. chrome.tabs.sendMessage(tab.id, {type: MSG_TYPE.PAGE_CAPTURE_SCROLL}, () => progress(0));
  242. }
  243. /**
  244. * 通过网页url生成默认的文件名
  245. * @param contentURL
  246. * @returns {string}
  247. */
  248. function buildFilenameFromUrl(contentURL) {
  249. let name = contentURL.split('?')[0].split('#')[0];
  250. if (name) {
  251. name = name
  252. .replace(/^https?:\/\//, '')
  253. .replace(/[^A-z0-9]+/g, '-')
  254. .replace(/-+/g, '-')
  255. .replace(/^[_\-]+/, '')
  256. .replace(/[_\-]+$/, '');
  257. name = '-' + name;
  258. } else {
  259. name = '';
  260. }
  261. return 'fehelper' + name + '-' + Date.now() + '.png';
  262. }
  263. /**
  264. * 截屏保存为文件
  265. * @param tab
  266. * @param callback
  267. * @param errback
  268. * @param progress
  269. */
  270. function captureToFiles(tab, callback, errback, progress) {
  271. let doneback = (screenshots) => {
  272. let blobs = getBlobs(screenshots);
  273. let i = 0;
  274. let len = blobs.length;
  275. // 生成临时文件名
  276. let baseName = buildFilenameFromUrl(tab.url);
  277. // 保存 & 打开
  278. (function doNext() {
  279. saveBlob(blobs[i], baseName, i, function (filename) {
  280. ++i >= len ? callback(filename) : doNext();
  281. }, errback);
  282. })();
  283. };
  284. captureToOrigin(tab, doneback, errback, progress);
  285. }
  286. /**
  287. * 截屏入口
  288. * @param tab
  289. */
  290. function fullPageCapture(tab) {
  291. // 配置项
  292. let captureConfig = {
  293. // 保存成功文件时,用这个
  294. successForFile: filename => {
  295. chrome.tabs.create({
  296. url: filename
  297. });
  298. },
  299. // 获取原始数据,用这个
  300. successForDataURI: function (screenshots) {
  301. capturedData = {
  302. pageInfo: tab,
  303. filename: buildFilenameFromUrl(tab.url),
  304. imageURI: screenshots.map(function (screenshot) {
  305. return screenshot.canvas.toDataURL();
  306. })
  307. };
  308. chrome.tabs.create({
  309. url: 'page-capture/index.html'
  310. });
  311. },
  312. fail: reason => {
  313. BgPageInstance.notify({
  314. title: '糟糕,转换失败',
  315. message: (reason && reason.message || reason || '稍后尝试刷新页面重试!')
  316. });
  317. },
  318. progress: percent => {
  319. percent = parseInt(percent * 100, 10) + '%';
  320. chrome.tabs.executeScript(tab.id, {
  321. code: 'document.title="进度:' + percent + ' ...";'
  322. });
  323. if (percent === '100%') {
  324. setTimeout(() => {
  325. chrome.tabs.executeScript(tab.id, {
  326. code: 'document.title="' + tab.title + '";'
  327. });
  328. }, 800);
  329. }
  330. }
  331. };
  332. // 截屏走起
  333. // captureToFiles(tab, captureConfig.successForFile, captureConfig.fail, captureConfig.progress);
  334. captureToOrigin(tab, captureConfig.successForDataURI, captureConfig.fail, captureConfig.progress);
  335. }
  336. function getCapturedData() {
  337. return capturedData;
  338. }
  339. return {
  340. full: fullPageCapture,
  341. getCapturedData: getCapturedData
  342. };
  343. };