|
| 1 | +import {validateAnnotation} from './annotations.mjs'; |
| 2 | +import {PDFDocument,PDFDict,PDFName,PDFHexString,PDFString,PDFArray,PDFNumber} from 'pdf-lib'; |
| 3 | +function text(dict,key){return dict.lookupMaybe(PDFName.of(key),PDFString,PDFHexString)?.decodeText()||'';} |
| 4 | +function numbers(dict,key){const array=dict.lookupMaybe(PDFName.of(key),PDFArray);return array?Array.from({length:array.size()},(_,i)=>array.lookup(i,PDFNumber).asNumber()):[];} |
| 5 | +function date(value){const match=/^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?/.exec(value);if(!match)return new Date().toISOString();return new Date(Date.UTC(+match[1],+(match[2]||1)-1,+(match[3]||1),+(match[4]||0),+(match[5]||0),+(match[6]||0))).toISOString();} |
| 6 | +function geometry(page){const crop=page.getCropBox(),rotation=((page.getRotation().angle%360)+360)%360;return {crop,rotation,width:rotation%180?crop.height:crop.width,height:rotation%180?crop.width:crop.height};} |
| 7 | +function viewport(x,y,g){const c=g.crop;if(g.rotation===90)return [y-c.y,x-c.x];if(g.rotation===180)return [c.x+c.width-x,y-c.y];if(g.rotation===270)return [c.y+c.height-y,c.x+c.width-x];return [x-c.x,c.y+c.height-y];} |
| 8 | +function bounds(points,g){const converted=points.map(([x,y])=>viewport(x,y,g)),xs=converted.map(p=>p[0]),ys=converted.map(p=>p[1]);const x=Math.max(0,Math.min(...xs)),y=Math.max(0,Math.min(...ys)),right=Math.min(g.width,Math.max(...xs)),bottom=Math.min(g.height,Math.max(...ys));return right>x&&bottom>y?{x,y,width:right-x,height:bottom-y}:null;} |
| 9 | +export async function importPDFAnnotations(bytes){ |
| 10 | + const pdf=await PDFDocument.load(bytes,{updateMetadata:false}),annotations=[],nativeRefs=[]; |
| 11 | + for(const [index,page] of pdf.getPages().entries()){ |
| 12 | + const array=page.node.Annots(),g=geometry(page);if(!array)continue; |
| 13 | + for(let i=0;i<array.size();i++){ |
| 14 | + const dict=array.lookupMaybe(i,PDFDict);if(!dict)continue; |
| 15 | + if(text(dict,'NM').startsWith('PDFMathReader:')){try{const a=JSON.parse(text(dict,'PDFMathReader'));if(a?.id)annotations.push(validateAnnotation(a));}catch{}continue;} |
| 16 | + const kind=dict.lookupMaybe(PDFName.of('Subtype'),PDFName)?.toString();if(!['/Highlight','/Text'].includes(kind))continue; |
| 17 | + const rect=numbers(dict,'Rect');if(rect.length!==4)continue; |
| 18 | + const sourceRect=bounds([[rect[0],rect[1]],[rect[2],rect[3]]],g);if(!sourceRect)continue; |
| 19 | + const quads=kind==='/Highlight'?numbers(dict,'QuadPoints'):[],rects=[]; |
| 20 | + for(let q=0;q+7<quads.length;q+=8){const r=bounds([[quads[q],quads[q+1]],[quads[q+2],quads[q+3]],[quads[q+4],quads[q+5]],[quads[q+6],quads[q+7]]],g);if(r)rects.push(r);} |
| 21 | + if(!rects.length)rects.push(sourceRect); |
| 22 | + const nativeRef=array.get(i).toString();if(!/^\d+ \d+ R$/.test(nativeRef))continue; |
| 23 | + const color=numbers(dict,'C');const rgb=color.length===3?color:color.length===1?[color[0],color[0],color[0]]:color.length===4?color.slice(0,3).map(c=>(1-c)*(1-color[3])):[1,.95,.42]; |
| 24 | + const hex='#'+rgb.map(v=>Math.round(Math.max(0,Math.min(1,v))*255).toString(16).padStart(2,'0')).join(''); |
| 25 | + const contents=text(dict,'Contents');annotations.push({id:`native:${nativeRef}`,nativeRef,page:index+1,kind:kind==='/Highlight'?'highlight':'comment',origin:'source',text:kind==='/Highlight'?contents:'',comment:kind==='/Text'?contents:'',color:hex,rects,sourceRect,createdAt:date(text(dict,'M')),author:text(dict,'T'),dateUnknown:!text(dict,'M')});nativeRefs.push(nativeRef); |
| 26 | + } |
| 27 | + } |
| 28 | + return {annotations,nativeRefs}; |
| 29 | +} |
0 commit comments