41 lines
2.9 KiB
JavaScript
41 lines
2.9 KiB
JavaScript
(()=>{
|
|
'use strict';
|
|
const PAGE_W=595.28, PAGE_H=841.89;
|
|
const enc=new TextEncoder();
|
|
const ascii=s=>enc.encode(s);
|
|
const concat=parts=>{const len=parts.reduce((n,p)=>n+p.length,0),out=new Uint8Array(len);let at=0;for(const p of parts){out.set(p,at);at+=p.length}return out};
|
|
const object=(id,body)=>{const b=body instanceof Uint8Array?body:ascii(body);return concat([ascii(`${id} 0 obj\n`),b,ascii('\nendobj\n')])};
|
|
function normalize(images){
|
|
if(!Array.isArray(images)||!images.length)throw new Error('PDF: нет страниц для экспорта.');
|
|
return images.map((img,index)=>{
|
|
const width=Math.max(1,Math.trunc(Number(img?.width)||0)),height=Math.max(1,Math.trunc(Number(img?.height)||0));
|
|
const bytes=img?.bytes instanceof Uint8Array?img.bytes:null;
|
|
if(!bytes?.length||!width||!height)throw new Error(`PDF: некорректная страница ${index+1}.`);
|
|
const ratio=width/height,target=PAGE_W/PAGE_H;
|
|
if(Math.abs(ratio-target)>.015)console.warn(`PDF page ${index+1}: aspect ratio ${ratio.toFixed(4)} differs from A4 ${target.toFixed(4)}`);
|
|
return {width,height,bytes};
|
|
});
|
|
}
|
|
function fromJpegs(input){
|
|
const images=normalize(input),objCount=2+images.length*3,objects=new Array(objCount+1);
|
|
objects[1]=object(1,'<< /Type /Catalog /Pages 2 0 R >>');
|
|
const kids=images.map((_,i)=>`${3+i*3} 0 R`).join(' ');
|
|
objects[2]=object(2,`<< /Type /Pages /Count ${images.length} /Kids [${kids}] >>`);
|
|
images.forEach((img,i)=>{
|
|
const pageId=3+i*3,imageId=4+i*3,contentId=5+i*3;
|
|
const content=`q\n${PAGE_W} 0 0 ${PAGE_H} 0 0 cm\n/Im1 Do\nQ\n`;
|
|
objects[pageId]=object(pageId,`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${PAGE_W} ${PAGE_H}] /Resources << /XObject << /Im1 ${imageId} 0 R >> >> /Contents ${contentId} 0 R >>`);
|
|
objects[imageId]=object(imageId,concat([ascii(`<< /Type /XObject /Subtype /Image /Width ${img.width} /Height ${img.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${img.bytes.length} >>\nstream\n`),img.bytes,ascii('\nendstream')]));
|
|
const cb=ascii(content);objects[contentId]=object(contentId,concat([ascii(`<< /Length ${cb.length} >>\nstream\n`),cb,ascii('endstream')]));
|
|
});
|
|
const header=concat([ascii('%PDF-1.4\n%'),new Uint8Array([255,255,255,255]),ascii('\n')]);
|
|
const offsets=new Array(objCount+1).fill(0),parts=[header];let pos=header.length;
|
|
for(let id=1;id<=objCount;id++){offsets[id]=pos;parts.push(objects[id]);pos+=objects[id].length}
|
|
const xrefPos=pos;let xref=`xref\n0 ${objCount+1}\n0000000000 65535 f \n`;
|
|
for(let id=1;id<=objCount;id++)xref+=String(offsets[id]).padStart(10,'0')+' 00000 n \n';
|
|
parts.push(ascii(xref+`trailer\n<< /Size ${objCount+1} /Root 1 0 R >>\nstartxref\n${xrefPos}\n%%EOF\n`));
|
|
return new Blob(parts,{type:'application/pdf'});
|
|
}
|
|
window.SunPdfEngine=Object.freeze({PAGE_W,PAGE_H,fromJpegs});
|
|
})();
|