Scanした紙のノートをScrapboxに取り込むUserScript
裁断した書籍にも使える
使い方
pdfファイルをuploadすると、scrapboxにimportできるjson fileを生成してdownloadする
pdfの解像度を指定できる
初期値は600
600より大きい値を指定する意味はない
また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でつけるだけでもいいのでは?
でも前後のリンクもあると読みやすいか

じゃあつけるか。
→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と結びつかない?
変だな


解像度が粗い?
デジタル作成した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.jsasync 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.jsconst sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));