generated at
Scanした紙のノートをScrapboxに取り込むUserScript
裁断した書籍にも使える

使い方
pdfファイルをuploadすると、scrapboxにimportできるjson fileを生成してdownloadする
pdfの解像度を指定できる
初期値は600
600より大きい値を指定する意味はない
ScanSnap S1500で読み取れる最大解像度は600dpiまで
また600だとuploadが非常に遅くなるので注意すること
手書きメモをuploadするときには600くらい必要
400か500でもいいかもしれないが、未確認
デジタル形式のPDF (講義ノートとか)であれば、150で十分
150と300とでほとんど差がない

実装
pdfの読み込みとGyazoへのupload
一つづつawaitで待ってuploadするようにした
uploadの順番と、ページ番号の順番とを一致させたい
2021-01-28 00:02:26 ……これ一致させる必要あるかな?
全部一斉に送っちゃえ!
json fileにしてdownloadする
問題はレイアウトか。
1 pageごとにScrapbox pageに分ける
タイトル
PDFの名前 + ページ番号
\ ${title} ${pageNum}\ でいいか。
メタ情報をどうやって入れる?
PDFのテキストを勝手にScrapboxに挿入できるようにするととても楽
2021-01-24 19:28:40 方法が見つからないので断念
とりあえずタイトルにノート名を入れるとして
ノート名+ページ番号
前後のページに対するリンクも貼っておく?
ノート名をhashtagでつけるだけでもいいのでは?
でも前後のリンクもあると読みやすいかtakker
じゃあつけるか。
→cf.xxxのリンクが貼ってあったら、そのページへのリンクも手動で書く

2021-02-15
23:12:54 PDFの中にテキストが含まれていたら、それを引用形式で書き出すようにした
2021-01-29
19:14:27 解像度を指定できるようにした
2021-01-25
23:42:21
更新・作成日時を秒単位に直して代入するようにした
一部の関数を別ページに切り出した

known issue
Gyazo accountと結びつかない?
変だなtakker
done解像度が粗い?
デジタル作成したPDFは問題ない
scanしたPDFの解像度が粗くなっている?
これを試してみよう。
解決したが、逆にupload速度が落ちてしまった
途中でCSPエラーが発生して失敗する
大きなPDFをuploadしているときに起きる
失敗したページをretryする機能が必要

作業ログ
いや、切り出すときにprivateかpublicかの判断ができないから難しいか。

dependencies
script.js
/* MIT License Copyright (c) 2020 ci7lus */ import '../use-pdfjs/script.js'; import '../PDFをGyazoにあげてScrapboxに取り込むUserScript/progressBar.js'; import {toArrayBuffer} from '../Blob.arrayBuffer()/script.js'; import {pdfPage2ImageDataURI} from '../pdfPage2ImageDataURI/script.js'; import {uploadToGyazoEasyAuth} from '../uploadToGyazoEasyAuth/script.js'; import {getLocalFiles} from '../簡単にfileをbrowserに取り込むscript/script.js'; import {downloadObject} from '../web_browserから任意のデータをdownloadするscript/script.js'; import {throttle} from '../promise-parallel-throttle/script.js'; scrapbox.PageMenu.addMenu({ title: 'upload-pdf', image: 'https://img.icons8.com/ios/180/FFFFFF/pdf.png', onClick: async () => { const file = await getLocalFiles({accept: 'application/pdf,.pdf'}); if (!file) return; console.log(file); const resolution = (()=>{ let value = prompt('Type the print resolution.', 600); if (value === null) return value; while (isNaN(parseInt(value))) { value = prompt('Invalid number.\nType the print resolution.', 600); if (value === null) return value; } return parseInt(value); })(); if (resolution === null) return; // 進捗表示エリアを作る const progressArea = document.createElement('progress-area'); document.body.appendChild(progressArea); const imageDataList = await pdf2GyazoDataList(file, resolution, progressArea); progressArea.message = 'Create json data...'; const pageData = {pages: imageDataList.map(({imageURL,text}, index) => { index++; const title = `${file.name} ${index}`; const nextLink = index === imageDataList.length ? '': `[${file.name} ${index + 1}]`; const previousLink = index === 1 ? '': `[${file.name} ${index - 1}]`; const quotes = text.trim() !== '' ? text.split(/\n/).flatMap(line => { if (/^\s*$/.test(line)) return []; return [`> ${line.replace(/(?<!\w)\s+(?=\W)/g, '')}`]; }) : []; return { title, created: file.lastModified / 1000, updated: file.lastModified / 1000, lines: [ title, `[[${imageURL}]]`, `<=${previousLink} | ${nextLink}=>`, ...quotes, ], }; }),}; downloadObject(pageData); progressArea.message = 'Done.'; document.body.removeChild(progressArea); }, });

PDFを読み込み、Gyazoに順番を維持してuploadする関数
script.js
async function pdf2GyazoDataList(pdfFile, printResolution, progressArea) {
PDFを読み込む
script.js
progressArea.message = 'Loading pdf file...'; try { const pdf = await pdfjsLib.getDocument({ data: await toArrayBuffer(pdfFile), // cors制限のためcmapsはstorage.googleapis.comに上げたものを用いる cMapUrl: 'https://storage.googleapis.com/chrono-lexica/ci7lus-assets/pdfjs/cmaps/', cMapPacked: true, }).promise; const metadata = await pdf.getMetadata(); console.log(metadata); progressArea.message = 'pdf file loaded. Uploading to gyazo...';
script.js
let progress = 0; const updateProgressText = () => { progressArea.message = `Uploading... (${progress++}/${ pdf.numPages })`; }; updateProgressText();
Gyazoにuploadする
10ページづつ一気に送る
ページ番号は1から始まるようだ
script.js
let promise = undefined; const uploadCallback = i => (async () => { console.log(`[page ${i + 1}] Converting a pdf page to a png image...`); const page = await pdf.getPage(i + 1); const textPromise = page.getTextContent(); const dataURI = await pdfPage2ImageDataURI(page, {printResolution}); console.log(`[page ${i + 1}] Converted. Uploading an image to Gyazo...`); try { const imageURL = await uploadToGyazoEasyAuth({ dataURI, title: pdfFile.name, clientId: 'fd84cef882b9b51d8de3365cd28c86bc2cc8a1646ef05dbc641ca49278f9d0d6', }); console.log(`[page ${i + 1}] Uploaded: ${imageURL}`); updateProgressText();
PDF中のテキストを取得する
改行を判定できないので、全て1行で取得する
script.js
const tokenizedText = await textPromise; return {imageURL, text: tokenizedText.items?.map?.(pageText => pageText.str)?.join?.('') ?? ''}; } catch(e) { console.error(`[page ${i + 1}] Failed to upload the page. `, e); console.log(`[page ${i + 1}] Will be tried to upload later.`); return undefined; } }); let imageDataList = await throttle([...Array(pdf.numPages).keys()] .map(i => uploadCallback(i)), {maxInProgress: 10});
uploadに失敗したページを再uploadする
script.js
while (imageDataList.some(data => data === undefined)) { imageDataList = await throttle(imageDataList.map((data, i) => async () => { if (data !== undefined) return data; console.log(`[page ${i + 1}] Retry to upload.`); return await uploadCallback(i)(); }), {maxInProgress: 10}); } progressArea.message = 'done'; return imageDataList; } catch (e) { console.error('failed', pdfFile, e); alert(`failed to load: ${pdfFile.name}`); } }

補助関数

指定した時間だけ待つ
script.js
const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));

#2021-03-13 06:02:25
#2021-02-15 23:13:34
#2021-01-29 19:21:20
#2021-01-28 01:43:42
#2021-01-25 23:56:12
#2021-01-24 19:20:55