Cpdf.php 214 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464
  1. <?php
  2. /**
  3. * A PHP class to provide the basic functionality to create a pdf document without
  4. * any requirement for additional modules.
  5. *
  6. * Extended by Orion Richardson to support Unicode / UTF-8 characters using
  7. * TCPDF and others as a guide.
  8. *
  9. * @author Wayne Munro <pdf@ros.co.nz>
  10. * @author Orion Richardson <orionr@yahoo.com>
  11. * @author Helmut Tischer <htischer@weihenstephan.org>
  12. * @author Ryan H. Masten <ryan.masten@gmail.com>
  13. * @author Brian Sweeney <eclecticgeek@gmail.com>
  14. * @author Fabien Ménager <fabien.menager@gmail.com>
  15. * @license Public Domain http://creativecommons.org/licenses/publicdomain/
  16. * @package Cpdf
  17. */
  18. namespace Dompdf;
  19. use FontLib\Exception\FontNotFoundException;
  20. use FontLib\Font;
  21. use FontLib\BinaryStream;
  22. class Cpdf
  23. {
  24. const PDF_VERSION = '1.7';
  25. const ACROFORM_SIG_SIGNATURESEXISTS = 0x0001;
  26. const ACROFORM_SIG_APPENDONLY = 0x0002;
  27. const ACROFORM_FIELD_BUTTON = 'Btn';
  28. const ACROFORM_FIELD_TEXT = 'Tx';
  29. const ACROFORM_FIELD_CHOICE = 'Ch';
  30. const ACROFORM_FIELD_SIG = 'Sig';
  31. const ACROFORM_FIELD_READONLY = 0x0001;
  32. const ACROFORM_FIELD_REQUIRED = 0x0002;
  33. const ACROFORM_FIELD_TEXT_MULTILINE = 0x1000;
  34. const ACROFORM_FIELD_TEXT_PASSWORD = 0x2000;
  35. const ACROFORM_FIELD_TEXT_RICHTEXT = 0x10000;
  36. const ACROFORM_FIELD_CHOICE_COMBO = 0x20000;
  37. const ACROFORM_FIELD_CHOICE_EDIT = 0x40000;
  38. const ACROFORM_FIELD_CHOICE_SORT = 0x80000;
  39. const ACROFORM_FIELD_CHOICE_MULTISELECT = 0x200000;
  40. const XOBJECT_SUBTYPE_FORM = 'Form';
  41. /**
  42. * @var integer The current number of pdf objects in the document
  43. */
  44. public $numObj = 0;
  45. /**
  46. * @var array This array contains all of the pdf objects, ready for final assembly
  47. */
  48. public $objects = [];
  49. /**
  50. * @var integer The objectId (number within the objects array) of the document catalog
  51. */
  52. public $catalogId;
  53. /**
  54. * @var integer The objectId (number within the objects array) of indirect references (Javascript EmbeddedFiles)
  55. */
  56. protected $indirectReferenceId = 0;
  57. /**
  58. * @var integer The objectId (number within the objects array)
  59. */
  60. protected $embeddedFilesId = 0;
  61. /**
  62. * AcroForm objectId
  63. *
  64. * @var integer
  65. */
  66. public $acroFormId;
  67. /**
  68. * @var int
  69. */
  70. public $signatureMaxLen = 5000;
  71. /**
  72. * @var array Array carrying information about the fonts that the system currently knows about
  73. * Used to ensure that a font is not loaded twice, among other things
  74. */
  75. public $fonts = [];
  76. /**
  77. * @var string The default font metrics file to use if no other font has been loaded.
  78. * The path to the directory containing the font metrics should be included
  79. */
  80. public $defaultFont = './fonts/Helvetica.afm';
  81. /**
  82. * @string A record of the current font
  83. */
  84. public $currentFont = '';
  85. /**
  86. * @var string The current base font
  87. */
  88. public $currentBaseFont = '';
  89. /**
  90. * @var integer The number of the current font within the font array
  91. */
  92. public $currentFontNum = 0;
  93. /**
  94. * @var integer
  95. */
  96. public $currentNode;
  97. /**
  98. * @var integer Object number of the current page
  99. */
  100. public $currentPage;
  101. /**
  102. * @var integer Object number of the currently active contents block
  103. */
  104. public $currentContents;
  105. /**
  106. * @var integer Number of fonts within the system
  107. */
  108. public $numFonts = 0;
  109. /**
  110. * @var integer Number of graphic state resources used
  111. */
  112. private $numStates = 0;
  113. /**
  114. * @var array Number of graphic state resources used
  115. */
  116. private $gstates = [];
  117. /**
  118. * @var array Current color for fill operations, defaults to inactive value,
  119. * all three components should be between 0 and 1 inclusive when active
  120. */
  121. public $currentColor = null;
  122. /**
  123. * @var array Current color for stroke operations (lines etc.)
  124. */
  125. public $currentStrokeColor = null;
  126. /**
  127. * @var string Fill rule (nonzero or evenodd)
  128. */
  129. public $fillRule = "nonzero";
  130. /**
  131. * @var string Current style that lines are drawn in
  132. */
  133. public $currentLineStyle = '';
  134. /**
  135. * @var array Current line transparency (partial graphics state)
  136. */
  137. public $currentLineTransparency = ["mode" => "Normal", "opacity" => 1.0];
  138. /**
  139. * array Current fill transparency (partial graphics state)
  140. */
  141. public $currentFillTransparency = ["mode" => "Normal", "opacity" => 1.0];
  142. /**
  143. * @var array An array which is used to save the state of the document, mainly the colors and styles
  144. * it is used to temporarily change to another state, then change back to what it was before
  145. */
  146. public $stateStack = [];
  147. /**
  148. * @var integer Number of elements within the state stack
  149. */
  150. public $nStateStack = 0;
  151. /**
  152. * @var integer Number of page objects within the document
  153. */
  154. public $numPages = 0;
  155. /**
  156. * @var array Object Id storage stack
  157. */
  158. public $stack = [];
  159. /**
  160. * @var integer Number of elements within the object Id storage stack
  161. */
  162. public $nStack = 0;
  163. /**
  164. * an array which contains information about the objects which are not firmly attached to pages
  165. * these have been added with the addObject function
  166. */
  167. public $looseObjects = [];
  168. /**
  169. * array contains information about how the loose objects are to be added to the document
  170. */
  171. public $addLooseObjects = [];
  172. /**
  173. * @var integer The objectId of the information object for the document
  174. * this contains authorship, title etc.
  175. */
  176. public $infoObject = 0;
  177. /**
  178. * @var integer Number of images being tracked within the document
  179. */
  180. public $numImages = 0;
  181. /**
  182. * @var array An array containing options about the document
  183. * it defaults to turning on the compression of the objects
  184. */
  185. public $options = ['compression' => true];
  186. /**
  187. * @var integer The objectId of the first page of the document
  188. */
  189. public $firstPageId;
  190. /**
  191. * @var integer The object Id of the procset object
  192. */
  193. public $procsetObjectId;
  194. /**
  195. * @var array Store the information about the relationship between font families
  196. * this used so that the code knows which font is the bold version of another font, etc.
  197. * the value of this array is initialised in the constructor function.
  198. */
  199. public $fontFamilies = [];
  200. /**
  201. * @var string Folder for php serialized formats of font metrics files.
  202. * If empty string, use same folder as original metrics files.
  203. * This can be passed in from class creator.
  204. * If this folder does not exist or is not writable, Cpdf will be **much** slower.
  205. * Because of potential trouble with php safe mode, folder cannot be created at runtime.
  206. */
  207. public $fontcache = '';
  208. /**
  209. * @var integer The version of the font metrics cache file.
  210. * This value must be manually incremented whenever the internal font data structure is modified.
  211. */
  212. public $fontcacheVersion = 6;
  213. /**
  214. * @var string Temporary folder.
  215. * If empty string, will attempt system tmp folder.
  216. * This can be passed in from class creator.
  217. */
  218. public $tmp = '';
  219. /**
  220. * @var string Track if the current font is bolded or italicised
  221. */
  222. public $currentTextState = '';
  223. /**
  224. * @var string Messages are stored here during processing, these can be selected afterwards to give some useful debug information
  225. */
  226. public $messages = '';
  227. /**
  228. * @var string The encryption array for the document encryption is stored here
  229. */
  230. public $arc4 = '';
  231. /**
  232. * @var integer The object Id of the encryption information
  233. */
  234. public $arc4_objnum = 0;
  235. /**
  236. * @var string The file identifier, used to uniquely identify a pdf document
  237. */
  238. public $fileIdentifier = '';
  239. /**
  240. * @var boolean A flag to say if a document is to be encrypted or not
  241. */
  242. public $encrypted = false;
  243. /**
  244. * @var string The encryption key for the encryption of all the document content (structure is not encrypted)
  245. */
  246. public $encryptionKey = '';
  247. /**
  248. * @var array Array which forms a stack to keep track of nested callback functions
  249. */
  250. public $callback = [];
  251. /**
  252. * @var integer The number of callback functions in the callback array
  253. */
  254. public $nCallback = 0;
  255. /**
  256. * @var array Store label->id pairs for named destinations, these will be used to replace internal links
  257. * done this way so that destinations can be defined after the location that links to them
  258. */
  259. public $destinations = [];
  260. /**
  261. * @var array Store the stack for the transaction commands, each item in here is a record of the values of all the
  262. * publiciables within the class, so that the user can rollback at will (from each 'start' command)
  263. * note that this includes the objects array, so these can be large.
  264. */
  265. public $checkpoint = '';
  266. /**
  267. * @var array Table of Image origin filenames and image labels which were already added with o_image().
  268. * Allows to merge identical images
  269. */
  270. public $imagelist = [];
  271. /**
  272. * @var boolean Whether the text passed in should be treated as Unicode or just local character set.
  273. */
  274. public $isUnicode = false;
  275. /**
  276. * @var string the JavaScript code of the document
  277. */
  278. public $javascript = '';
  279. /**
  280. * @var boolean whether the compression is possible
  281. */
  282. protected $compressionReady = false;
  283. /**
  284. * @var array Current page size
  285. */
  286. protected $currentPageSize = ["width" => 0, "height" => 0];
  287. /**
  288. * @var array All the chars that will be required in the font subsets
  289. */
  290. protected $stringSubsets = [];
  291. /**
  292. * @var string The target internal encoding
  293. */
  294. protected static $targetEncoding = 'Windows-1252';
  295. /**
  296. * @var array
  297. */
  298. protected $byteRange = array();
  299. /**
  300. * @var array The list of the core fonts
  301. */
  302. protected static $coreFonts = [
  303. 'courier',
  304. 'courier-bold',
  305. 'courier-oblique',
  306. 'courier-boldoblique',
  307. 'helvetica',
  308. 'helvetica-bold',
  309. 'helvetica-oblique',
  310. 'helvetica-boldoblique',
  311. 'times-roman',
  312. 'times-bold',
  313. 'times-italic',
  314. 'times-bolditalic',
  315. 'symbol',
  316. 'zapfdingbats'
  317. ];
  318. /**
  319. * Class constructor
  320. * This will start a new document
  321. *
  322. * @param array $pageSize Array of 4 numbers, defining the bottom left and upper right corner of the page. first two are normally zero.
  323. * @param boolean $isUnicode Whether text will be treated as Unicode or not.
  324. * @param string $fontcache The font cache folder
  325. * @param string $tmp The temporary folder
  326. */
  327. function __construct($pageSize = [0, 0, 612, 792], $isUnicode = false, $fontcache = '', $tmp = '')
  328. {
  329. $this->isUnicode = $isUnicode;
  330. $this->fontcache = rtrim($fontcache, DIRECTORY_SEPARATOR."/\\");
  331. $this->tmp = ($tmp !== '' ? $tmp : sys_get_temp_dir());
  332. $this->newDocument($pageSize);
  333. $this->compressionReady = function_exists('gzcompress');
  334. if (in_array('Windows-1252', mb_list_encodings())) {
  335. self::$targetEncoding = 'Windows-1252';
  336. }
  337. // also initialize the font families that are known about already
  338. $this->setFontFamily('init');
  339. }
  340. /**
  341. * Document object methods (internal use only)
  342. *
  343. * There is about one object method for each type of object in the pdf document
  344. * Each function has the same call list ($id,$action,$options).
  345. * $id = the object ID of the object, or what it is to be if it is being created
  346. * $action = a string specifying the action to be performed, though ALL must support:
  347. * 'new' - create the object with the id $id
  348. * 'out' - produce the output for the pdf object
  349. * $options = optional, a string or array containing the various parameters for the object
  350. *
  351. * These, in conjunction with the output function are the ONLY way for output to be produced
  352. * within the pdf 'file'.
  353. */
  354. /**
  355. * Destination object, used to specify the location for the user to jump to, presently on opening
  356. *
  357. * @param $id
  358. * @param $action
  359. * @param string $options
  360. * @return string|null
  361. */
  362. protected function o_destination($id, $action, $options = '')
  363. {
  364. switch ($action) {
  365. case 'new':
  366. $this->objects[$id] = ['t' => 'destination', 'info' => []];
  367. $tmp = '';
  368. switch ($options['type']) {
  369. case 'XYZ':
  370. /** @noinspection PhpMissingBreakStatementInspection */
  371. case 'FitR':
  372. $tmp = ' ' . $options['p3'] . $tmp;
  373. case 'FitH':
  374. case 'FitV':
  375. case 'FitBH':
  376. /** @noinspection PhpMissingBreakStatementInspection */
  377. case 'FitBV':
  378. $tmp = ' ' . $options['p1'] . ' ' . $options['p2'] . $tmp;
  379. case 'Fit':
  380. case 'FitB':
  381. $tmp = $options['type'] . $tmp;
  382. $this->objects[$id]['info']['string'] = $tmp;
  383. $this->objects[$id]['info']['page'] = $options['page'];
  384. }
  385. break;
  386. case 'out':
  387. $o = &$this->objects[$id];
  388. $tmp = $o['info'];
  389. $res = "\n$id 0 obj\n" . '[' . $tmp['page'] . ' 0 R /' . $tmp['string'] . "]\nendobj";
  390. return $res;
  391. }
  392. return null;
  393. }
  394. /**
  395. * set the viewer preferences
  396. *
  397. * @param $id
  398. * @param $action
  399. * @param string|array $options
  400. * @return string|null
  401. */
  402. protected function o_viewerPreferences($id, $action, $options = '')
  403. {
  404. switch ($action) {
  405. case 'new':
  406. $this->objects[$id] = ['t' => 'viewerPreferences', 'info' => []];
  407. break;
  408. case 'add':
  409. $o = &$this->objects[$id];
  410. foreach ($options as $k => $v) {
  411. switch ($k) {
  412. // Boolean keys
  413. case 'HideToolbar':
  414. case 'HideMenubar':
  415. case 'HideWindowUI':
  416. case 'FitWindow':
  417. case 'CenterWindow':
  418. case 'DisplayDocTitle':
  419. case 'PickTrayByPDFSize':
  420. $o['info'][$k] = (bool)$v;
  421. break;
  422. // Integer keys
  423. case 'NumCopies':
  424. $o['info'][$k] = (int)$v;
  425. break;
  426. // Name keys
  427. case 'ViewArea':
  428. case 'ViewClip':
  429. case 'PrintClip':
  430. case 'PrintArea':
  431. $o['info'][$k] = (string)$v;
  432. break;
  433. // Named with limited valid values
  434. case 'NonFullScreenPageMode':
  435. if (!in_array($v, ['UseNone', 'UseOutlines', 'UseThumbs', 'UseOC'])) {
  436. break;
  437. }
  438. $o['info'][$k] = $v;
  439. break;
  440. case 'Direction':
  441. if (!in_array($v, ['L2R', 'R2L'])) {
  442. break;
  443. }
  444. $o['info'][$k] = $v;
  445. break;
  446. case 'PrintScaling':
  447. if (!in_array($v, ['None', 'AppDefault'])) {
  448. break;
  449. }
  450. $o['info'][$k] = $v;
  451. break;
  452. case 'Duplex':
  453. if (!in_array($v, ['None', 'Simplex', 'DuplexFlipShortEdge', 'DuplexFlipLongEdge'])) {
  454. break;
  455. }
  456. $o['info'][$k] = $v;
  457. break;
  458. // Integer array
  459. case 'PrintPageRange':
  460. // Cast to integer array
  461. foreach ($v as $vK => $vV) {
  462. $v[$vK] = (int)$vV;
  463. }
  464. $o['info'][$k] = array_values($v);
  465. break;
  466. }
  467. }
  468. break;
  469. case 'out':
  470. $o = &$this->objects[$id];
  471. $res = "\n$id 0 obj\n<< ";
  472. foreach ($o['info'] as $k => $v) {
  473. if (is_string($v)) {
  474. $v = '/' . $v;
  475. } elseif (is_int($v)) {
  476. $v = (string) $v;
  477. } elseif (is_bool($v)) {
  478. $v = ($v ? 'true' : 'false');
  479. } elseif (is_array($v)) {
  480. $v = '[' . implode(' ', $v) . ']';
  481. }
  482. $res .= "\n/$k $v";
  483. }
  484. $res .= "\n>>\nendobj";
  485. return $res;
  486. }
  487. return null;
  488. }
  489. /**
  490. * define the document catalog, the overall controller for the document
  491. *
  492. * @param $id
  493. * @param $action
  494. * @param string|array $options
  495. * @return string|null
  496. */
  497. protected function o_catalog($id, $action, $options = '')
  498. {
  499. if ($action !== 'new') {
  500. $o = &$this->objects[$id];
  501. }
  502. switch ($action) {
  503. case 'new':
  504. $this->objects[$id] = ['t' => 'catalog', 'info' => []];
  505. $this->catalogId = $id;
  506. break;
  507. case 'acroform':
  508. case 'outlines':
  509. case 'pages':
  510. case 'openHere':
  511. case 'names':
  512. $o['info'][$action] = $options;
  513. break;
  514. case 'viewerPreferences':
  515. if (!isset($o['info']['viewerPreferences'])) {
  516. $this->numObj++;
  517. $this->o_viewerPreferences($this->numObj, 'new');
  518. $o['info']['viewerPreferences'] = $this->numObj;
  519. }
  520. $vp = $o['info']['viewerPreferences'];
  521. $this->o_viewerPreferences($vp, 'add', $options);
  522. break;
  523. case 'out':
  524. $res = "\n$id 0 obj\n<< /Type /Catalog";
  525. foreach ($o['info'] as $k => $v) {
  526. switch ($k) {
  527. case 'outlines':
  528. $res .= "\n/Outlines $v 0 R";
  529. break;
  530. case 'pages':
  531. $res .= "\n/Pages $v 0 R";
  532. break;
  533. case 'viewerPreferences':
  534. $res .= "\n/ViewerPreferences $v 0 R";
  535. break;
  536. case 'openHere':
  537. $res .= "\n/OpenAction $v 0 R";
  538. break;
  539. case 'names':
  540. $res .= "\n/Names $v 0 R";
  541. break;
  542. case 'acroform':
  543. $res .= "\n/AcroForm $v 0 R";
  544. break;
  545. }
  546. }
  547. $res .= " >>\nendobj";
  548. return $res;
  549. }
  550. return null;
  551. }
  552. /**
  553. * object which is a parent to the pages in the document
  554. *
  555. * @param $id
  556. * @param $action
  557. * @param string $options
  558. * @return string|null
  559. */
  560. protected function o_pages($id, $action, $options = '')
  561. {
  562. if ($action !== 'new') {
  563. $o = &$this->objects[$id];
  564. }
  565. switch ($action) {
  566. case 'new':
  567. $this->objects[$id] = ['t' => 'pages', 'info' => []];
  568. $this->o_catalog($this->catalogId, 'pages', $id);
  569. break;
  570. case 'page':
  571. if (!is_array($options)) {
  572. // then it will just be the id of the new page
  573. $o['info']['pages'][] = $options;
  574. } else {
  575. // then it should be an array having 'id','rid','pos', where rid=the page to which this one will be placed relative
  576. // and pos is either 'before' or 'after', saying where this page will fit.
  577. if (isset($options['id']) && isset($options['rid']) && isset($options['pos'])) {
  578. $i = array_search($options['rid'], $o['info']['pages']);
  579. if (isset($o['info']['pages'][$i]) && $o['info']['pages'][$i] == $options['rid']) {
  580. // then there is a match
  581. // make a space
  582. switch ($options['pos']) {
  583. case 'before':
  584. $k = $i;
  585. break;
  586. case 'after':
  587. $k = $i + 1;
  588. break;
  589. default:
  590. $k = -1;
  591. break;
  592. }
  593. if ($k >= 0) {
  594. for ($j = count($o['info']['pages']) - 1; $j >= $k; $j--) {
  595. $o['info']['pages'][$j + 1] = $o['info']['pages'][$j];
  596. }
  597. $o['info']['pages'][$k] = $options['id'];
  598. }
  599. }
  600. }
  601. }
  602. break;
  603. case 'procset':
  604. $o['info']['procset'] = $options;
  605. break;
  606. case 'mediaBox':
  607. $o['info']['mediaBox'] = $options;
  608. // which should be an array of 4 numbers
  609. $this->currentPageSize = ['width' => $options[2], 'height' => $options[3]];
  610. break;
  611. case 'font':
  612. $o['info']['fonts'][] = ['objNum' => $options['objNum'], 'fontNum' => $options['fontNum']];
  613. break;
  614. case 'extGState':
  615. $o['info']['extGStates'][] = ['objNum' => $options['objNum'], 'stateNum' => $options['stateNum']];
  616. break;
  617. case 'xObject':
  618. $o['info']['xObjects'][] = ['objNum' => $options['objNum'], 'label' => $options['label']];
  619. break;
  620. case 'out':
  621. if (count($o['info']['pages'])) {
  622. $res = "\n$id 0 obj\n<< /Type /Pages\n/Kids [";
  623. foreach ($o['info']['pages'] as $v) {
  624. $res .= "$v 0 R\n";
  625. }
  626. $res .= "]\n/Count " . count($this->objects[$id]['info']['pages']);
  627. if ((isset($o['info']['fonts']) && count($o['info']['fonts'])) ||
  628. isset($o['info']['procset']) ||
  629. (isset($o['info']['extGStates']) && count($o['info']['extGStates']))
  630. ) {
  631. $res .= "\n/Resources <<";
  632. if (isset($o['info']['procset'])) {
  633. $res .= "\n/ProcSet " . $o['info']['procset'] . " 0 R";
  634. }
  635. if (isset($o['info']['fonts']) && count($o['info']['fonts'])) {
  636. $res .= "\n/Font << ";
  637. foreach ($o['info']['fonts'] as $finfo) {
  638. $res .= "\n/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R";
  639. }
  640. $res .= "\n>>";
  641. }
  642. if (isset($o['info']['xObjects']) && count($o['info']['xObjects'])) {
  643. $res .= "\n/XObject << ";
  644. foreach ($o['info']['xObjects'] as $finfo) {
  645. $res .= "\n/" . $finfo['label'] . " " . $finfo['objNum'] . " 0 R";
  646. }
  647. $res .= "\n>>";
  648. }
  649. if (isset($o['info']['extGStates']) && count($o['info']['extGStates'])) {
  650. $res .= "\n/ExtGState << ";
  651. foreach ($o['info']['extGStates'] as $gstate) {
  652. $res .= "\n/GS" . $gstate['stateNum'] . " " . $gstate['objNum'] . " 0 R";
  653. }
  654. $res .= "\n>>";
  655. }
  656. $res .= "\n>>";
  657. if (isset($o['info']['mediaBox'])) {
  658. $tmp = $o['info']['mediaBox'];
  659. $res .= "\n/MediaBox [" . sprintf(
  660. '%.3F %.3F %.3F %.3F',
  661. $tmp[0],
  662. $tmp[1],
  663. $tmp[2],
  664. $tmp[3]
  665. ) . ']';
  666. }
  667. }
  668. $res .= "\n >>\nendobj";
  669. } else {
  670. $res = "\n$id 0 obj\n<< /Type /Pages\n/Count 0\n>>\nendobj";
  671. }
  672. return $res;
  673. }
  674. return null;
  675. }
  676. /**
  677. * define the outlines in the doc, empty for now
  678. *
  679. * @param $id
  680. * @param $action
  681. * @param string $options
  682. * @return string|null
  683. */
  684. protected function o_outlines($id, $action, $options = '')
  685. {
  686. if ($action !== 'new') {
  687. $o = &$this->objects[$id];
  688. }
  689. switch ($action) {
  690. case 'new':
  691. $this->objects[$id] = ['t' => 'outlines', 'info' => ['outlines' => []]];
  692. $this->o_catalog($this->catalogId, 'outlines', $id);
  693. break;
  694. case 'outline':
  695. $o['info']['outlines'][] = $options;
  696. break;
  697. case 'out':
  698. if (count($o['info']['outlines'])) {
  699. $res = "\n$id 0 obj\n<< /Type /Outlines /Kids [";
  700. foreach ($o['info']['outlines'] as $v) {
  701. $res .= "$v 0 R ";
  702. }
  703. $res .= "] /Count " . count($o['info']['outlines']) . " >>\nendobj";
  704. } else {
  705. $res = "\n$id 0 obj\n<< /Type /Outlines /Count 0 >>\nendobj";
  706. }
  707. return $res;
  708. }
  709. return null;
  710. }
  711. /**
  712. * an object to hold the font description
  713. *
  714. * @param $id
  715. * @param $action
  716. * @param string|array $options
  717. * @return string|null
  718. * @throws FontNotFoundException
  719. */
  720. protected function o_font($id, $action, $options = '')
  721. {
  722. if ($action !== 'new') {
  723. $o = &$this->objects[$id];
  724. }
  725. switch ($action) {
  726. case 'new':
  727. $this->objects[$id] = [
  728. 't' => 'font',
  729. 'info' => [
  730. 'name' => $options['name'],
  731. 'fontFileName' => $options['fontFileName'],
  732. 'SubType' => 'Type1',
  733. 'isSubsetting' => $options['isSubsetting']
  734. ]
  735. ];
  736. $fontNum = $this->numFonts;
  737. $this->objects[$id]['info']['fontNum'] = $fontNum;
  738. // deal with the encoding and the differences
  739. if (isset($options['differences'])) {
  740. // then we'll need an encoding dictionary
  741. $this->numObj++;
  742. $this->o_fontEncoding($this->numObj, 'new', $options);
  743. $this->objects[$id]['info']['encodingDictionary'] = $this->numObj;
  744. } else {
  745. if (isset($options['encoding'])) {
  746. // we can specify encoding here
  747. switch ($options['encoding']) {
  748. case 'WinAnsiEncoding':
  749. case 'MacRomanEncoding':
  750. case 'MacExpertEncoding':
  751. $this->objects[$id]['info']['encoding'] = $options['encoding'];
  752. break;
  753. case 'none':
  754. break;
  755. default:
  756. $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding';
  757. break;
  758. }
  759. } else {
  760. $this->objects[$id]['info']['encoding'] = 'WinAnsiEncoding';
  761. }
  762. }
  763. if ($this->fonts[$options['fontFileName']]['isUnicode']) {
  764. // For Unicode fonts, we need to incorporate font data into
  765. // sub-sections that are linked from the primary font section.
  766. // Look at o_fontGIDtoCID and o_fontDescendentCID functions
  767. // for more information.
  768. //
  769. // All of this code is adapted from the excellent changes made to
  770. // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/)
  771. $toUnicodeId = ++$this->numObj;
  772. $this->o_toUnicode($toUnicodeId, 'new');
  773. $this->objects[$id]['info']['toUnicode'] = $toUnicodeId;
  774. $cidFontId = ++$this->numObj;
  775. $this->o_fontDescendentCID($cidFontId, 'new', $options);
  776. $this->objects[$id]['info']['cidFont'] = $cidFontId;
  777. }
  778. // also tell the pages node about the new font
  779. $this->o_pages($this->currentNode, 'font', ['fontNum' => $fontNum, 'objNum' => $id]);
  780. break;
  781. case 'add':
  782. $font_options = $this->processFont($id, $o['info']);
  783. if ($font_options !== false) {
  784. foreach ($font_options as $k => $v) {
  785. switch ($k) {
  786. case 'BaseFont':
  787. $o['info']['name'] = $v;
  788. break;
  789. case 'FirstChar':
  790. case 'LastChar':
  791. case 'Widths':
  792. case 'FontDescriptor':
  793. case 'SubType':
  794. $this->addMessage('o_font ' . $k . " : " . $v);
  795. $o['info'][$k] = $v;
  796. break;
  797. }
  798. }
  799. // pass values down to descendent font
  800. if (isset($o['info']['cidFont'])) {
  801. $this->o_fontDescendentCID($o['info']['cidFont'], 'add', $font_options);
  802. }
  803. }
  804. break;
  805. case 'out':
  806. if ($this->fonts[$this->objects[$id]['info']['fontFileName']]['isUnicode']) {
  807. // For Unicode fonts, we need to incorporate font data into
  808. // sub-sections that are linked from the primary font section.
  809. // Look at o_fontGIDtoCID and o_fontDescendentCID functions
  810. // for more information.
  811. //
  812. // All of this code is adapted from the excellent changes made to
  813. // transform FPDF to TCPDF (http://tcpdf.sourceforge.net/)
  814. $res = "\n$id 0 obj\n<</Type /Font\n/Subtype /Type0\n";
  815. $res .= "/BaseFont /" . $o['info']['name'] . "\n";
  816. // The horizontal identity mapping for 2-byte CIDs; may be used
  817. // with CIDFonts using any Registry, Ordering, and Supplement values.
  818. $res .= "/Encoding /Identity-H\n";
  819. $res .= "/DescendantFonts [" . $o['info']['cidFont'] . " 0 R]\n";
  820. $res .= "/ToUnicode " . $o['info']['toUnicode'] . " 0 R\n";
  821. $res .= ">>\n";
  822. $res .= "endobj";
  823. } else {
  824. $res = "\n$id 0 obj\n<< /Type /Font\n/Subtype /" . $o['info']['SubType'] . "\n";
  825. $res .= "/Name /F" . $o['info']['fontNum'] . "\n";
  826. $res .= "/BaseFont /" . $o['info']['name'] . "\n";
  827. if (isset($o['info']['encodingDictionary'])) {
  828. // then place a reference to the dictionary
  829. $res .= "/Encoding " . $o['info']['encodingDictionary'] . " 0 R\n";
  830. } else {
  831. if (isset($o['info']['encoding'])) {
  832. // use the specified encoding
  833. $res .= "/Encoding /" . $o['info']['encoding'] . "\n";
  834. }
  835. }
  836. if (isset($o['info']['FirstChar'])) {
  837. $res .= "/FirstChar " . $o['info']['FirstChar'] . "\n";
  838. }
  839. if (isset($o['info']['LastChar'])) {
  840. $res .= "/LastChar " . $o['info']['LastChar'] . "\n";
  841. }
  842. if (isset($o['info']['Widths'])) {
  843. $res .= "/Widths " . $o['info']['Widths'] . " 0 R\n";
  844. }
  845. if (isset($o['info']['FontDescriptor'])) {
  846. $res .= "/FontDescriptor " . $o['info']['FontDescriptor'] . " 0 R\n";
  847. }
  848. $res .= ">>\n";
  849. $res .= "endobj";
  850. }
  851. return $res;
  852. }
  853. return null;
  854. }
  855. protected function getFontSubsettingTag(array $font): string
  856. {
  857. // convert font num to hexavigesimal numeral system letters A - Z only
  858. $base_26 = strtoupper(base_convert($font['fontNum'], 10, 26));
  859. for ($i = 0; $i < strlen($base_26); $i++) {
  860. $char = $base_26[$i];
  861. if ($char <= "9") {
  862. $base_26[$i] = chr(65 + intval($char));
  863. } else {
  864. $base_26[$i] = chr(ord($char) + 10);
  865. }
  866. }
  867. return 'SUB' . str_pad($base_26, 3 , 'A', STR_PAD_LEFT);
  868. }
  869. /**
  870. * @param int $fontObjId
  871. * @param array $object_info
  872. * @return array|false
  873. * @throws FontNotFoundException
  874. */
  875. private function processFont(int $fontObjId, array $object_info)
  876. {
  877. $fontFileName = $object_info['fontFileName'];
  878. if (!isset($this->fonts[$fontFileName])) {
  879. return false;
  880. }
  881. $font = &$this->fonts[$fontFileName];
  882. $fileSuffix = $font['fileSuffix'];
  883. $fileSuffixLower = strtolower($font['fileSuffix']);
  884. $fbfile = "$fontFileName.$fileSuffix";
  885. $isTtfFont = $fileSuffixLower === 'ttf';
  886. $isPfbFont = $fileSuffixLower === 'pfb';
  887. $this->addMessage('selectFont: checking for - ' . $fbfile);
  888. if (!$fileSuffix) {
  889. $this->addMessage(
  890. 'selectFont: pfb or ttf file not found, ok if this is one of the 14 standard fonts'
  891. );
  892. return false;
  893. } else {
  894. $adobeFontName = isset($font['PostScriptName']) ? $font['PostScriptName'] : $font['FontName'];
  895. // $fontObj = $this->numObj;
  896. $this->addMessage("selectFont: adding font file - $fbfile - $adobeFontName");
  897. // find the array of font widths, and put that into an object.
  898. $firstChar = -1;
  899. $lastChar = 0;
  900. $widths = [];
  901. $cid_widths = [];
  902. foreach ($font['C'] as $num => $d) {
  903. if (intval($num) > 0 || $num == '0') {
  904. if (!$font['isUnicode']) {
  905. // With Unicode, widths array isn't used
  906. if ($lastChar > 0 && $num > $lastChar + 1) {
  907. for ($i = $lastChar + 1; $i < $num; $i++) {
  908. $widths[] = 0;
  909. }
  910. }
  911. }
  912. $widths[] = $d;
  913. if ($font['isUnicode']) {
  914. $cid_widths[$num] = $d;
  915. }
  916. if ($firstChar == -1) {
  917. $firstChar = $num;
  918. }
  919. $lastChar = $num;
  920. }
  921. }
  922. // also need to adjust the widths for the differences array
  923. if (isset($object['differences'])) {
  924. foreach ($object['differences'] as $charNum => $charName) {
  925. if ($charNum > $lastChar) {
  926. if (!$object['isUnicode']) {
  927. // With Unicode, widths array isn't used
  928. for ($i = $lastChar + 1; $i <= $charNum; $i++) {
  929. $widths[] = 0;
  930. }
  931. }
  932. $lastChar = $charNum;
  933. }
  934. if (isset($font['C'][$charName])) {
  935. $widths[$charNum - $firstChar] = $font['C'][$charName];
  936. if ($font['isUnicode']) {
  937. $cid_widths[$charName] = $font['C'][$charName];
  938. }
  939. }
  940. }
  941. }
  942. if ($font['isUnicode']) {
  943. $font['CIDWidths'] = $cid_widths;
  944. }
  945. $this->addMessage('selectFont: FirstChar = ' . $firstChar);
  946. $this->addMessage('selectFont: LastChar = ' . $lastChar);
  947. $widthid = -1;
  948. if (!$font['isUnicode']) {
  949. // With Unicode, widths array isn't used
  950. $this->numObj++;
  951. $this->o_contents($this->numObj, 'new', 'raw');
  952. $this->objects[$this->numObj]['c'] .= '[' . implode(' ', $widths) . ']';
  953. $widthid = $this->numObj;
  954. }
  955. $missing_width = 500;
  956. $stemV = 70;
  957. if (isset($font['MissingWidth'])) {
  958. $missing_width = $font['MissingWidth'];
  959. }
  960. if (isset($font['StdVW'])) {
  961. $stemV = $font['StdVW'];
  962. } else {
  963. if (isset($font['Weight']) && preg_match('!(bold|black)!i', $font['Weight'])) {
  964. $stemV = 120;
  965. }
  966. }
  967. // load the pfb file, and put that into an object too.
  968. // note that pdf supports only binary format type 1 font files, though there is a
  969. // simple utility to convert them from pfa to pfb.
  970. if (!$font['isSubsetting']) {
  971. $data = file_get_contents($fbfile);
  972. } else {
  973. $adobeFontName = $this->getFontSubsettingTag($font) . '+' . $adobeFontName;
  974. $this->stringSubsets[$fontFileName][] = 32; // Force space if not in yet
  975. $subset = $this->stringSubsets[$fontFileName];
  976. sort($subset);
  977. // Load font
  978. $font_obj = Font::load($fbfile);
  979. $font_obj->parse();
  980. // Define subset
  981. $font_obj->setSubset($subset);
  982. $font_obj->reduce();
  983. // Write new font
  984. $tmp_name = $this->tmp . "/" . basename($fbfile) . ".tmp." . uniqid();
  985. touch($tmp_name);
  986. $font_obj->open($tmp_name, BinaryStream::modeReadWrite);
  987. $font_obj->encode(["OS/2"]);
  988. $font_obj->close();
  989. // Parse the new font to get cid2gid and widths
  990. $font_obj = Font::load($tmp_name);
  991. // Find Unicode char map table
  992. $subtable = null;
  993. foreach ($font_obj->getData("cmap", "subtables") as $_subtable) {
  994. if ($_subtable["platformID"] == 0 || $_subtable["platformID"] == 3 && $_subtable["platformSpecificID"] == 1) {
  995. $subtable = $_subtable;
  996. break;
  997. }
  998. }
  999. if ($subtable) {
  1000. $glyphIndexArray = $subtable["glyphIndexArray"];
  1001. $hmtx = $font_obj->getData("hmtx");
  1002. unset($glyphIndexArray[0xFFFF]);
  1003. $cidtogid = str_pad('', max(array_keys($glyphIndexArray)) * 2 + 1, "\x00");
  1004. $font['CIDWidths'] = [];
  1005. foreach ($glyphIndexArray as $cid => $gid) {
  1006. if ($cid >= 0 && $cid < 0xFFFF && $gid) {
  1007. $cidtogid[$cid * 2] = chr($gid >> 8);
  1008. $cidtogid[$cid * 2 + 1] = chr($gid & 0xFF);
  1009. }
  1010. $width = $font_obj->normalizeFUnit(isset($hmtx[$gid]) ? $hmtx[$gid][0] : $hmtx[0][0]);
  1011. $font['CIDWidths'][$cid] = $width;
  1012. }
  1013. $font['CIDtoGID'] = base64_encode(gzcompress($cidtogid));
  1014. $font['CIDtoGID_Compressed'] = true;
  1015. $data = file_get_contents($tmp_name);
  1016. } else {
  1017. $data = file_get_contents($fbfile);
  1018. }
  1019. $font_obj->close();
  1020. unlink($tmp_name);
  1021. }
  1022. // create the font descriptor
  1023. $this->numObj++;
  1024. $fontDescriptorId = $this->numObj;
  1025. $this->numObj++;
  1026. $pfbid = $this->numObj;
  1027. // determine flags (more than a little flakey, hopefully will not matter much)
  1028. $flags = 0;
  1029. if ($font['ItalicAngle'] != 0) {
  1030. $flags += pow(2, 6);
  1031. }
  1032. if ($font['IsFixedPitch'] === 'true') {
  1033. $flags += 1;
  1034. }
  1035. $flags += pow(2, 5); // assume non-sybolic
  1036. $list = [
  1037. 'Ascent' => 'Ascender',
  1038. 'CapHeight' => 'Ascender', //FIXME: php-font-lib is not grabbing this value, so we'll fake it and use the Ascender value // 'CapHeight'
  1039. 'MissingWidth' => 'MissingWidth',
  1040. 'Descent' => 'Descender',
  1041. 'FontBBox' => 'FontBBox',
  1042. 'ItalicAngle' => 'ItalicAngle'
  1043. ];
  1044. $fdopt = [
  1045. 'Flags' => $flags,
  1046. 'FontName' => $adobeFontName,
  1047. 'StemV' => $stemV
  1048. ];
  1049. foreach ($list as $k => $v) {
  1050. if (isset($font[$v])) {
  1051. $fdopt[$k] = $font[$v];
  1052. }
  1053. }
  1054. if ($isPfbFont) {
  1055. $fdopt['FontFile'] = $pfbid;
  1056. } elseif ($isTtfFont) {
  1057. $fdopt['FontFile2'] = $pfbid;
  1058. }
  1059. $this->o_fontDescriptor($fontDescriptorId, 'new', $fdopt);
  1060. // embed the font program
  1061. $this->o_contents($this->numObj, 'new');
  1062. $this->objects[$pfbid]['c'] .= $data;
  1063. // determine the cruicial lengths within this file
  1064. if ($isPfbFont) {
  1065. $l1 = strpos($data, 'eexec') + 6;
  1066. $l2 = strpos($data, '00000000') - $l1;
  1067. $l3 = mb_strlen($data, '8bit') - $l2 - $l1;
  1068. $this->o_contents(
  1069. $this->numObj,
  1070. 'add',
  1071. ['Length1' => $l1, 'Length2' => $l2, 'Length3' => $l3]
  1072. );
  1073. } elseif ($isTtfFont) {
  1074. $l1 = mb_strlen($data, '8bit');
  1075. $this->o_contents($this->numObj, 'add', ['Length1' => $l1]);
  1076. }
  1077. // tell the font object about all this new stuff
  1078. $options = [
  1079. 'BaseFont' => $adobeFontName,
  1080. 'MissingWidth' => $missing_width,
  1081. 'Widths' => $widthid,
  1082. 'FirstChar' => $firstChar,
  1083. 'LastChar' => $lastChar,
  1084. 'FontDescriptor' => $fontDescriptorId
  1085. ];
  1086. if ($isTtfFont) {
  1087. $options['SubType'] = 'TrueType';
  1088. }
  1089. $this->addMessage("adding extra info to font.($fontObjId)");
  1090. foreach ($options as $fk => $fv) {
  1091. $this->addMessage("$fk : $fv");
  1092. }
  1093. }
  1094. return $options;
  1095. }
  1096. /**
  1097. * A toUnicode section, needed for unicode fonts
  1098. *
  1099. * @param $id
  1100. * @param $action
  1101. * @return null|string
  1102. */
  1103. protected function o_toUnicode($id, $action)
  1104. {
  1105. switch ($action) {
  1106. case 'new':
  1107. $this->objects[$id] = [
  1108. 't' => 'toUnicode'
  1109. ];
  1110. break;
  1111. case 'add':
  1112. break;
  1113. case 'out':
  1114. $ordering = 'UCS';
  1115. $registry = 'Adobe';
  1116. if ($this->encrypted) {
  1117. $this->encryptInit($id);
  1118. $ordering = $this->ARC4($ordering);
  1119. $registry = $this->filterText($this->ARC4($registry), false, false);
  1120. }
  1121. $stream = <<<EOT
  1122. /CIDInit /ProcSet findresource begin
  1123. 12 dict begin
  1124. begincmap
  1125. /CIDSystemInfo
  1126. <</Registry ($registry)
  1127. /Ordering ($ordering)
  1128. /Supplement 0
  1129. >> def
  1130. /CMapName /Adobe-Identity-UCS def
  1131. /CMapType 2 def
  1132. 1 begincodespacerange
  1133. <0000> <FFFF>
  1134. endcodespacerange
  1135. 1 beginbfrange
  1136. <0000> <FFFF> <0000>
  1137. endbfrange
  1138. endcmap
  1139. CMapName currentdict /CMap defineresource pop
  1140. end
  1141. end
  1142. EOT;
  1143. $res = "\n$id 0 obj\n";
  1144. $res .= "<</Length " . mb_strlen($stream, '8bit') . " >>\n";
  1145. $res .= "stream\n" . $stream . "\nendstream" . "\nendobj";;
  1146. return $res;
  1147. }
  1148. return null;
  1149. }
  1150. /**
  1151. * a font descriptor, needed for including additional fonts
  1152. *
  1153. * @param $id
  1154. * @param $action
  1155. * @param string $options
  1156. * @return null|string
  1157. */
  1158. protected function o_fontDescriptor($id, $action, $options = '')
  1159. {
  1160. if ($action !== 'new') {
  1161. $o = &$this->objects[$id];
  1162. }
  1163. switch ($action) {
  1164. case 'new':
  1165. $this->objects[$id] = ['t' => 'fontDescriptor', 'info' => $options];
  1166. break;
  1167. case 'out':
  1168. $res = "\n$id 0 obj\n<< /Type /FontDescriptor\n";
  1169. foreach ($o['info'] as $label => $value) {
  1170. switch ($label) {
  1171. case 'Ascent':
  1172. case 'CapHeight':
  1173. case 'Descent':
  1174. case 'Flags':
  1175. case 'ItalicAngle':
  1176. case 'StemV':
  1177. case 'AvgWidth':
  1178. case 'Leading':
  1179. case 'MaxWidth':
  1180. case 'MissingWidth':
  1181. case 'StemH':
  1182. case 'XHeight':
  1183. case 'CharSet':
  1184. if (mb_strlen($value, '8bit')) {
  1185. $res .= "/$label $value\n";
  1186. }
  1187. break;
  1188. case 'FontFile':
  1189. case 'FontFile2':
  1190. case 'FontFile3':
  1191. $res .= "/$label $value 0 R\n";
  1192. break;
  1193. case 'FontBBox':
  1194. $res .= "/$label [$value[0] $value[1] $value[2] $value[3]]\n";
  1195. break;
  1196. case 'FontName':
  1197. $res .= "/$label /$value\n";
  1198. break;
  1199. }
  1200. }
  1201. $res .= ">>\nendobj";
  1202. return $res;
  1203. }
  1204. return null;
  1205. }
  1206. /**
  1207. * the font encoding
  1208. *
  1209. * @param $id
  1210. * @param $action
  1211. * @param string $options
  1212. * @return null|string
  1213. */
  1214. protected function o_fontEncoding($id, $action, $options = '')
  1215. {
  1216. if ($action !== 'new') {
  1217. $o = &$this->objects[$id];
  1218. }
  1219. switch ($action) {
  1220. case 'new':
  1221. // the options array should contain 'differences' and maybe 'encoding'
  1222. $this->objects[$id] = ['t' => 'fontEncoding', 'info' => $options];
  1223. break;
  1224. case 'out':
  1225. $res = "\n$id 0 obj\n<< /Type /Encoding\n";
  1226. if (!isset($o['info']['encoding'])) {
  1227. $o['info']['encoding'] = 'WinAnsiEncoding';
  1228. }
  1229. if ($o['info']['encoding'] !== 'none') {
  1230. $res .= "/BaseEncoding /" . $o['info']['encoding'] . "\n";
  1231. }
  1232. $res .= "/Differences \n[";
  1233. $onum = -100;
  1234. foreach ($o['info']['differences'] as $num => $label) {
  1235. if ($num != $onum + 1) {
  1236. // we cannot make use of consecutive numbering
  1237. $res .= "\n$num /$label";
  1238. } else {
  1239. $res .= " /$label";
  1240. }
  1241. $onum = $num;
  1242. }
  1243. $res .= "\n]\n>>\nendobj";
  1244. return $res;
  1245. }
  1246. return null;
  1247. }
  1248. /**
  1249. * a descendent cid font, needed for unicode fonts
  1250. *
  1251. * @param $id
  1252. * @param $action
  1253. * @param string|array $options
  1254. * @return null|string
  1255. */
  1256. protected function o_fontDescendentCID($id, $action, $options = '')
  1257. {
  1258. if ($action !== 'new') {
  1259. $o = &$this->objects[$id];
  1260. }
  1261. switch ($action) {
  1262. case 'new':
  1263. $this->objects[$id] = ['t' => 'fontDescendentCID', 'info' => $options];
  1264. // we need a CID system info section
  1265. $cidSystemInfoId = ++$this->numObj;
  1266. $this->o_cidSystemInfo($cidSystemInfoId, 'new');
  1267. $this->objects[$id]['info']['cidSystemInfo'] = $cidSystemInfoId;
  1268. // and a CID to GID map
  1269. $cidToGidMapId = ++$this->numObj;
  1270. $this->o_fontGIDtoCIDMap($cidToGidMapId, 'new', $options);
  1271. $this->objects[$id]['info']['cidToGidMap'] = $cidToGidMapId;
  1272. break;
  1273. case 'add':
  1274. foreach ($options as $k => $v) {
  1275. switch ($k) {
  1276. case 'BaseFont':
  1277. $o['info']['name'] = $v;
  1278. break;
  1279. case 'FirstChar':
  1280. case 'LastChar':
  1281. case 'MissingWidth':
  1282. case 'FontDescriptor':
  1283. case 'SubType':
  1284. $this->addMessage("o_fontDescendentCID $k : $v");
  1285. $o['info'][$k] = $v;
  1286. break;
  1287. }
  1288. }
  1289. // pass values down to cid to gid map
  1290. $this->o_fontGIDtoCIDMap($o['info']['cidToGidMap'], 'add', $options);
  1291. break;
  1292. case 'out':
  1293. $res = "\n$id 0 obj\n";
  1294. $res .= "<</Type /Font\n";
  1295. $res .= "/Subtype /CIDFontType2\n";
  1296. $res .= "/BaseFont /" . $o['info']['name'] . "\n";
  1297. $res .= "/CIDSystemInfo " . $o['info']['cidSystemInfo'] . " 0 R\n";
  1298. // if (isset($o['info']['FirstChar'])) {
  1299. // $res.= "/FirstChar ".$o['info']['FirstChar']."\n";
  1300. // }
  1301. // if (isset($o['info']['LastChar'])) {
  1302. // $res.= "/LastChar ".$o['info']['LastChar']."\n";
  1303. // }
  1304. if (isset($o['info']['FontDescriptor'])) {
  1305. $res .= "/FontDescriptor " . $o['info']['FontDescriptor'] . " 0 R\n";
  1306. }
  1307. if (isset($o['info']['MissingWidth'])) {
  1308. $res .= "/DW " . $o['info']['MissingWidth'] . "\n";
  1309. }
  1310. if (isset($o['info']['fontFileName']) && isset($this->fonts[$o['info']['fontFileName']]['CIDWidths'])) {
  1311. $cid_widths = &$this->fonts[$o['info']['fontFileName']]['CIDWidths'];
  1312. $w = '';
  1313. foreach ($cid_widths as $cid => $width) {
  1314. $w .= "$cid [$width] ";
  1315. }
  1316. $res .= "/W [$w]\n";
  1317. }
  1318. $res .= "/CIDToGIDMap " . $o['info']['cidToGidMap'] . " 0 R\n";
  1319. $res .= ">>\n";
  1320. $res .= "endobj";
  1321. return $res;
  1322. }
  1323. return null;
  1324. }
  1325. /**
  1326. * CID system info section, needed for unicode fonts
  1327. *
  1328. * @param $id
  1329. * @param $action
  1330. * @return null|string
  1331. */
  1332. protected function o_cidSystemInfo($id, $action)
  1333. {
  1334. switch ($action) {
  1335. case 'new':
  1336. $this->objects[$id] = [
  1337. 't' => 'cidSystemInfo'
  1338. ];
  1339. break;
  1340. case 'add':
  1341. break;
  1342. case 'out':
  1343. $ordering = 'UCS';
  1344. $registry = 'Adobe';
  1345. if ($this->encrypted) {
  1346. $this->encryptInit($id);
  1347. $ordering = $this->ARC4($ordering);
  1348. $registry = $this->ARC4($registry);
  1349. }
  1350. $res = "\n$id 0 obj\n";
  1351. $res .= '<</Registry (' . $registry . ")\n"; // A string identifying an issuer of character collections
  1352. $res .= '/Ordering (' . $ordering . ")\n"; // A string that uniquely names a character collection issued by a specific registry
  1353. $res .= "/Supplement 0\n"; // The supplement number of the character collection.
  1354. $res .= ">>";
  1355. $res .= "\nendobj";
  1356. return $res;
  1357. }
  1358. return null;
  1359. }
  1360. /**
  1361. * a font glyph to character map, needed for unicode fonts
  1362. *
  1363. * @param $id
  1364. * @param $action
  1365. * @param string $options
  1366. * @return null|string
  1367. */
  1368. protected function o_fontGIDtoCIDMap($id, $action, $options = '')
  1369. {
  1370. if ($action !== 'new') {
  1371. $o = &$this->objects[$id];
  1372. }
  1373. switch ($action) {
  1374. case 'new':
  1375. $this->objects[$id] = ['t' => 'fontGIDtoCIDMap', 'info' => $options];
  1376. break;
  1377. case 'out':
  1378. $res = "\n$id 0 obj\n";
  1379. $fontFileName = $o['info']['fontFileName'];
  1380. $tmp = $this->fonts[$fontFileName]['CIDtoGID'] = base64_decode($this->fonts[$fontFileName]['CIDtoGID']);
  1381. $compressed = isset($this->fonts[$fontFileName]['CIDtoGID_Compressed']) &&
  1382. $this->fonts[$fontFileName]['CIDtoGID_Compressed'];
  1383. if (!$compressed && isset($o['raw'])) {
  1384. $res .= $tmp;
  1385. } else {
  1386. $res .= "<<";
  1387. if (!$compressed && $this->compressionReady && $this->options['compression']) {
  1388. // then implement ZLIB based compression on this content stream
  1389. $compressed = true;
  1390. $tmp = gzcompress($tmp, 6);
  1391. }
  1392. if ($compressed) {
  1393. $res .= "\n/Filter /FlateDecode";
  1394. }
  1395. if ($this->encrypted) {
  1396. $this->encryptInit($id);
  1397. $tmp = $this->ARC4($tmp);
  1398. }
  1399. $res .= "\n/Length " . mb_strlen($tmp, '8bit') . ">>\nstream\n$tmp\nendstream";
  1400. }
  1401. $res .= "\nendobj";
  1402. return $res;
  1403. }
  1404. return null;
  1405. }
  1406. /**
  1407. * the document procset, solves some problems with printing to old PS printers
  1408. *
  1409. * @param $id
  1410. * @param $action
  1411. * @param string $options
  1412. * @return null|string
  1413. */
  1414. protected function o_procset($id, $action, $options = '')
  1415. {
  1416. if ($action !== 'new') {
  1417. $o = &$this->objects[$id];
  1418. }
  1419. switch ($action) {
  1420. case 'new':
  1421. $this->objects[$id] = ['t' => 'procset', 'info' => ['PDF' => 1, 'Text' => 1]];
  1422. $this->o_pages($this->currentNode, 'procset', $id);
  1423. $this->procsetObjectId = $id;
  1424. break;
  1425. case 'add':
  1426. // this is to add new items to the procset list, despite the fact that this is considered
  1427. // obsolete, the items are required for printing to some postscript printers
  1428. switch ($options) {
  1429. case 'ImageB':
  1430. case 'ImageC':
  1431. case 'ImageI':
  1432. $o['info'][$options] = 1;
  1433. break;
  1434. }
  1435. break;
  1436. case 'out':
  1437. $res = "\n$id 0 obj\n[";
  1438. foreach ($o['info'] as $label => $val) {
  1439. $res .= "/$label ";
  1440. }
  1441. $res .= "]\nendobj";
  1442. return $res;
  1443. }
  1444. return null;
  1445. }
  1446. /**
  1447. * define the document information
  1448. *
  1449. * @param $id
  1450. * @param $action
  1451. * @param string $options
  1452. * @return null|string
  1453. */
  1454. protected function o_info($id, $action, $options = '')
  1455. {
  1456. switch ($action) {
  1457. case 'new':
  1458. $this->infoObject = $id;
  1459. $date = 'D:' . @date('Ymd');
  1460. $this->objects[$id] = [
  1461. 't' => 'info',
  1462. 'info' => [
  1463. 'Producer' => 'CPDF (dompdf)',
  1464. 'CreationDate' => $date
  1465. ]
  1466. ];
  1467. break;
  1468. case 'Title':
  1469. case 'Author':
  1470. case 'Subject':
  1471. case 'Keywords':
  1472. case 'Creator':
  1473. case 'Producer':
  1474. case 'CreationDate':
  1475. case 'ModDate':
  1476. case 'Trapped':
  1477. $this->objects[$id]['info'][$action] = $options;
  1478. break;
  1479. case 'out':
  1480. $encrypted = $this->encrypted;
  1481. if ($encrypted) {
  1482. $this->encryptInit($id);
  1483. }
  1484. $res = "\n$id 0 obj\n<<\n";
  1485. $o = &$this->objects[$id];
  1486. foreach ($o['info'] as $k => $v) {
  1487. $res .= "/$k (";
  1488. // dates must be outputted as-is, without Unicode transformations
  1489. if ($k !== 'CreationDate' && $k !== 'ModDate') {
  1490. $v = $this->filterText($v, true, false);
  1491. }
  1492. if ($encrypted) {
  1493. $v = $this->ARC4($v);
  1494. }
  1495. $res .= $v;
  1496. $res .= ")\n";
  1497. }
  1498. $res .= ">>\nendobj";
  1499. return $res;
  1500. }
  1501. return null;
  1502. }
  1503. /**
  1504. * an action object, used to link to URLS initially
  1505. *
  1506. * @param $id
  1507. * @param $action
  1508. * @param string $options
  1509. * @return null|string
  1510. */
  1511. protected function o_action($id, $action, $options = '')
  1512. {
  1513. if ($action !== 'new') {
  1514. $o = &$this->objects[$id];
  1515. }
  1516. switch ($action) {
  1517. case 'new':
  1518. if (is_array($options)) {
  1519. $this->objects[$id] = ['t' => 'action', 'info' => $options, 'type' => $options['type']];
  1520. } else {
  1521. // then assume a URI action
  1522. $this->objects[$id] = ['t' => 'action', 'info' => $options, 'type' => 'URI'];
  1523. }
  1524. break;
  1525. case 'out':
  1526. if ($this->encrypted) {
  1527. $this->encryptInit($id);
  1528. }
  1529. $res = "\n$id 0 obj\n<< /Type /Action";
  1530. switch ($o['type']) {
  1531. case 'ilink':
  1532. if (!isset($this->destinations[(string)$o['info']['label']])) {
  1533. break;
  1534. }
  1535. // there will be an 'label' setting, this is the name of the destination
  1536. $res .= "\n/S /GoTo\n/D " . $this->destinations[(string)$o['info']['label']] . " 0 R";
  1537. break;
  1538. case 'URI':
  1539. $res .= "\n/S /URI\n/URI (";
  1540. if ($this->encrypted) {
  1541. $res .= $this->filterText($this->ARC4($o['info']), false, false);
  1542. } else {
  1543. $res .= $this->filterText($o['info'], false, false);
  1544. }
  1545. $res .= ")";
  1546. break;
  1547. }
  1548. $res .= "\n>>\nendobj";
  1549. return $res;
  1550. }
  1551. return null;
  1552. }
  1553. /**
  1554. * an annotation object, this will add an annotation to the current page.
  1555. * initially will support just link annotations
  1556. *
  1557. * @param $id
  1558. * @param $action
  1559. * @param string $options
  1560. * @return null|string
  1561. */
  1562. protected function o_annotation($id, $action, $options = '')
  1563. {
  1564. if ($action !== 'new') {
  1565. $o = &$this->objects[$id];
  1566. }
  1567. switch ($action) {
  1568. case 'new':
  1569. // add the annotation to the current page
  1570. $pageId = $this->currentPage;
  1571. $this->o_page($pageId, 'annot', $id);
  1572. // and add the action object which is going to be required
  1573. switch ($options['type']) {
  1574. case 'link':
  1575. $this->objects[$id] = ['t' => 'annotation', 'info' => $options];
  1576. $this->numObj++;
  1577. $this->o_action($this->numObj, 'new', $options['url']);
  1578. $this->objects[$id]['info']['actionId'] = $this->numObj;
  1579. break;
  1580. case 'ilink':
  1581. // this is to a named internal link
  1582. $label = $options['label'];
  1583. $this->objects[$id] = ['t' => 'annotation', 'info' => $options];
  1584. $this->numObj++;
  1585. $this->o_action($this->numObj, 'new', ['type' => 'ilink', 'label' => $label]);
  1586. $this->objects[$id]['info']['actionId'] = $this->numObj;
  1587. break;
  1588. }
  1589. break;
  1590. case 'out':
  1591. $res = "\n$id 0 obj\n<< /Type /Annot";
  1592. switch ($o['info']['type']) {
  1593. case 'link':
  1594. case 'ilink':
  1595. $res .= "\n/Subtype /Link";
  1596. break;
  1597. }
  1598. $res .= "\n/A " . $o['info']['actionId'] . " 0 R";
  1599. $res .= "\n/Border [0 0 0]";
  1600. $res .= "\n/H /I";
  1601. $res .= "\n/Rect [ ";
  1602. foreach ($o['info']['rect'] as $v) {
  1603. $res .= sprintf("%.4F ", $v);
  1604. }
  1605. $res .= "]";
  1606. $res .= "\n>>\nendobj";
  1607. return $res;
  1608. }
  1609. return null;
  1610. }
  1611. /**
  1612. * a page object, it also creates a contents object to hold its contents
  1613. *
  1614. * @param $id
  1615. * @param $action
  1616. * @param string $options
  1617. * @return null|string
  1618. */
  1619. protected function o_page($id, $action, $options = '')
  1620. {
  1621. if ($action !== 'new') {
  1622. $o = &$this->objects[$id];
  1623. }
  1624. switch ($action) {
  1625. case 'new':
  1626. $this->numPages++;
  1627. $this->objects[$id] = [
  1628. 't' => 'page',
  1629. 'info' => [
  1630. 'parent' => $this->currentNode,
  1631. 'pageNum' => $this->numPages,
  1632. 'mediaBox' => $this->objects[$this->currentNode]['info']['mediaBox']
  1633. ]
  1634. ];
  1635. if (is_array($options)) {
  1636. // then this must be a page insertion, array should contain 'rid','pos'=[before|after]
  1637. $options['id'] = $id;
  1638. $this->o_pages($this->currentNode, 'page', $options);
  1639. } else {
  1640. $this->o_pages($this->currentNode, 'page', $id);
  1641. }
  1642. $this->currentPage = $id;
  1643. //make a contents object to go with this page
  1644. $this->numObj++;
  1645. $this->o_contents($this->numObj, 'new', $id);
  1646. $this->currentContents = $this->numObj;
  1647. $this->objects[$id]['info']['contents'] = [];
  1648. $this->objects[$id]['info']['contents'][] = $this->numObj;
  1649. $match = ($this->numPages % 2 ? 'odd' : 'even');
  1650. foreach ($this->addLooseObjects as $oId => $target) {
  1651. if ($target === 'all' || $match === $target) {
  1652. $this->objects[$id]['info']['contents'][] = $oId;
  1653. }
  1654. }
  1655. break;
  1656. case 'content':
  1657. $o['info']['contents'][] = $options;
  1658. break;
  1659. case 'annot':
  1660. // add an annotation to this page
  1661. if (!isset($o['info']['annot'])) {
  1662. $o['info']['annot'] = [];
  1663. }
  1664. // $options should contain the id of the annotation dictionary
  1665. $o['info']['annot'][] = $options;
  1666. break;
  1667. case 'out':
  1668. $res = "\n$id 0 obj\n<< /Type /Page";
  1669. if (isset($o['info']['mediaBox'])) {
  1670. $tmp = $o['info']['mediaBox'];
  1671. $res .= "\n/MediaBox [" . sprintf(
  1672. '%.3F %.3F %.3F %.3F',
  1673. $tmp[0],
  1674. $tmp[1],
  1675. $tmp[2],
  1676. $tmp[3]
  1677. ) . ']';
  1678. }
  1679. $res .= "\n/Parent " . $o['info']['parent'] . " 0 R";
  1680. if (isset($o['info']['annot'])) {
  1681. $res .= "\n/Annots [";
  1682. foreach ($o['info']['annot'] as $aId) {
  1683. $res .= " $aId 0 R";
  1684. }
  1685. $res .= " ]";
  1686. }
  1687. $count = count($o['info']['contents']);
  1688. if ($count == 1) {
  1689. $res .= "\n/Contents " . $o['info']['contents'][0] . " 0 R";
  1690. } else {
  1691. if ($count > 1) {
  1692. $res .= "\n/Contents [\n";
  1693. // reverse the page contents so added objects are below normal content
  1694. //foreach (array_reverse($o['info']['contents']) as $cId) {
  1695. // Back to normal now that I've got transparency working --Benj
  1696. foreach ($o['info']['contents'] as $cId) {
  1697. $res .= "$cId 0 R\n";
  1698. }
  1699. $res .= "]";
  1700. }
  1701. }
  1702. $res .= "\n>>\nendobj";
  1703. return $res;
  1704. }
  1705. return null;
  1706. }
  1707. /**
  1708. * the contents objects hold all of the content which appears on pages
  1709. *
  1710. * @param $id
  1711. * @param $action
  1712. * @param string|array $options
  1713. * @return null|string
  1714. */
  1715. protected function o_contents($id, $action, $options = '')
  1716. {
  1717. if ($action !== 'new') {
  1718. $o = &$this->objects[$id];
  1719. }
  1720. switch ($action) {
  1721. case 'new':
  1722. $this->objects[$id] = ['t' => 'contents', 'c' => '', 'info' => []];
  1723. if (mb_strlen($options, '8bit') && intval($options)) {
  1724. // then this contents is the primary for a page
  1725. $this->objects[$id]['onPage'] = $options;
  1726. } else {
  1727. if ($options === 'raw') {
  1728. // then this page contains some other type of system object
  1729. $this->objects[$id]['raw'] = 1;
  1730. }
  1731. }
  1732. break;
  1733. case 'add':
  1734. // add more options to the declaration
  1735. foreach ($options as $k => $v) {
  1736. $o['info'][$k] = $v;
  1737. }
  1738. case 'out':
  1739. $tmp = $o['c'];
  1740. $res = "\n$id 0 obj\n";
  1741. if (isset($this->objects[$id]['raw'])) {
  1742. $res .= $tmp;
  1743. } else {
  1744. $res .= "<<";
  1745. if ($this->compressionReady && $this->options['compression']) {
  1746. // then implement ZLIB based compression on this content stream
  1747. $res .= " /Filter /FlateDecode";
  1748. $tmp = gzcompress($tmp, 6);
  1749. }
  1750. if ($this->encrypted) {
  1751. $this->encryptInit($id);
  1752. $tmp = $this->ARC4($tmp);
  1753. }
  1754. foreach ($o['info'] as $k => $v) {
  1755. $res .= "\n/$k $v";
  1756. }
  1757. $res .= "\n/Length " . mb_strlen($tmp, '8bit') . " >>\nstream\n$tmp\nendstream";
  1758. }
  1759. $res .= "\nendobj";
  1760. return $res;
  1761. }
  1762. return null;
  1763. }
  1764. /**
  1765. * @param $id
  1766. * @param $action
  1767. * @return string|null
  1768. */
  1769. protected function o_embedjs($id, $action)
  1770. {
  1771. switch ($action) {
  1772. case 'new':
  1773. $this->objects[$id] = [
  1774. 't' => 'embedjs',
  1775. 'info' => [
  1776. 'Names' => '[(EmbeddedJS) ' . ($id + 1) . ' 0 R]'
  1777. ]
  1778. ];
  1779. break;
  1780. case 'out':
  1781. $o = &$this->objects[$id];
  1782. $res = "\n$id 0 obj\n<< ";
  1783. foreach ($o['info'] as $k => $v) {
  1784. $res .= "\n/$k $v";
  1785. }
  1786. $res .= "\n>>\nendobj";
  1787. return $res;
  1788. }
  1789. return null;
  1790. }
  1791. /**
  1792. * @param $id
  1793. * @param $action
  1794. * @param string $code
  1795. * @return null|string
  1796. */
  1797. protected function o_javascript($id, $action, $code = '')
  1798. {
  1799. switch ($action) {
  1800. case 'new':
  1801. $this->objects[$id] = [
  1802. 't' => 'javascript',
  1803. 'info' => [
  1804. 'S' => '/JavaScript',
  1805. 'JS' => '(' . $this->filterText($code, true, false) . ')',
  1806. ]
  1807. ];
  1808. break;
  1809. case 'out':
  1810. $o = &$this->objects[$id];
  1811. $res = "\n$id 0 obj\n<< ";
  1812. foreach ($o['info'] as $k => $v) {
  1813. $res .= "\n/$k $v";
  1814. }
  1815. $res .= "\n>>\nendobj";
  1816. return $res;
  1817. }
  1818. return null;
  1819. }
  1820. /**
  1821. * an image object, will be an XObject in the document, includes description and data
  1822. *
  1823. * @param $id
  1824. * @param $action
  1825. * @param string $options
  1826. * @return null|string
  1827. */
  1828. protected function o_image($id, $action, $options = '')
  1829. {
  1830. switch ($action) {
  1831. case 'new':
  1832. // make the new object
  1833. $this->objects[$id] = ['t' => 'image', 'data' => &$options['data'], 'info' => []];
  1834. $info =& $this->objects[$id]['info'];
  1835. $info['Type'] = '/XObject';
  1836. $info['Subtype'] = '/Image';
  1837. $info['Width'] = $options['iw'];
  1838. $info['Height'] = $options['ih'];
  1839. if (isset($options['masked']) && $options['masked']) {
  1840. $info['SMask'] = ($this->numObj - 1) . ' 0 R';
  1841. }
  1842. if (!isset($options['type']) || $options['type'] === 'jpg') {
  1843. if (!isset($options['channels'])) {
  1844. $options['channels'] = 3;
  1845. }
  1846. switch ($options['channels']) {
  1847. case 1:
  1848. $info['ColorSpace'] = '/DeviceGray';
  1849. break;
  1850. case 4:
  1851. $info['ColorSpace'] = '/DeviceCMYK';
  1852. break;
  1853. default:
  1854. $info['ColorSpace'] = '/DeviceRGB';
  1855. break;
  1856. }
  1857. if ($info['ColorSpace'] === '/DeviceCMYK') {
  1858. $info['Decode'] = '[1 0 1 0 1 0 1 0]';
  1859. }
  1860. $info['Filter'] = '/DCTDecode';
  1861. $info['BitsPerComponent'] = 8;
  1862. } else {
  1863. if ($options['type'] === 'png') {
  1864. $info['Filter'] = '/FlateDecode';
  1865. $info['DecodeParms'] = '<< /Predictor 15 /Colors ' . $options['ncolor'] . ' /Columns ' . $options['iw'] . ' /BitsPerComponent ' . $options['bitsPerComponent'] . '>>';
  1866. if ($options['isMask']) {
  1867. $info['ColorSpace'] = '/DeviceGray';
  1868. } else {
  1869. if (mb_strlen($options['pdata'], '8bit')) {
  1870. $tmp = ' [ /Indexed /DeviceRGB ' . (mb_strlen($options['pdata'], '8bit') / 3 - 1) . ' ';
  1871. $this->numObj++;
  1872. $this->o_contents($this->numObj, 'new');
  1873. $this->objects[$this->numObj]['c'] = $options['pdata'];
  1874. $tmp .= $this->numObj . ' 0 R';
  1875. $tmp .= ' ]';
  1876. $info['ColorSpace'] = $tmp;
  1877. if (isset($options['transparency'])) {
  1878. $transparency = $options['transparency'];
  1879. switch ($transparency['type']) {
  1880. case 'indexed':
  1881. $tmp = ' [ ' . $transparency['data'] . ' ' . $transparency['data'] . '] ';
  1882. $info['Mask'] = $tmp;
  1883. break;
  1884. case 'color-key':
  1885. $tmp = ' [ ' .
  1886. $transparency['r'] . ' ' . $transparency['r'] .
  1887. $transparency['g'] . ' ' . $transparency['g'] .
  1888. $transparency['b'] . ' ' . $transparency['b'] .
  1889. ' ] ';
  1890. $info['Mask'] = $tmp;
  1891. break;
  1892. }
  1893. }
  1894. } else {
  1895. if (isset($options['transparency'])) {
  1896. $transparency = $options['transparency'];
  1897. switch ($transparency['type']) {
  1898. case 'indexed':
  1899. $tmp = ' [ ' . $transparency['data'] . ' ' . $transparency['data'] . '] ';
  1900. $info['Mask'] = $tmp;
  1901. break;
  1902. case 'color-key':
  1903. $tmp = ' [ ' .
  1904. $transparency['r'] . ' ' . $transparency['r'] . ' ' .
  1905. $transparency['g'] . ' ' . $transparency['g'] . ' ' .
  1906. $transparency['b'] . ' ' . $transparency['b'] .
  1907. ' ] ';
  1908. $info['Mask'] = $tmp;
  1909. break;
  1910. }
  1911. }
  1912. $info['ColorSpace'] = '/' . $options['color'];
  1913. }
  1914. }
  1915. $info['BitsPerComponent'] = $options['bitsPerComponent'];
  1916. }
  1917. }
  1918. // assign it a place in the named resource dictionary as an external object, according to
  1919. // the label passed in with it.
  1920. $this->o_pages($this->currentNode, 'xObject', ['label' => $options['label'], 'objNum' => $id]);
  1921. // also make sure that we have the right procset object for it.
  1922. $this->o_procset($this->procsetObjectId, 'add', 'ImageC');
  1923. break;
  1924. case 'out':
  1925. $o = &$this->objects[$id];
  1926. $tmp = &$o['data'];
  1927. $res = "\n$id 0 obj\n<<";
  1928. foreach ($o['info'] as $k => $v) {
  1929. $res .= "\n/$k $v";
  1930. }
  1931. if ($this->encrypted) {
  1932. $this->encryptInit($id);
  1933. $tmp = $this->ARC4($tmp);
  1934. }
  1935. $res .= "\n/Length " . mb_strlen($tmp, '8bit') . ">>\nstream\n$tmp\nendstream\nendobj";
  1936. return $res;
  1937. }
  1938. return null;
  1939. }
  1940. /**
  1941. * graphics state object
  1942. *
  1943. * @param $id
  1944. * @param $action
  1945. * @param string $options
  1946. * @return null|string
  1947. */
  1948. protected function o_extGState($id, $action, $options = "")
  1949. {
  1950. static $valid_params = [
  1951. "LW",
  1952. "LC",
  1953. "LC",
  1954. "LJ",
  1955. "ML",
  1956. "D",
  1957. "RI",
  1958. "OP",
  1959. "op",
  1960. "OPM",
  1961. "Font",
  1962. "BG",
  1963. "BG2",
  1964. "UCR",
  1965. "TR",
  1966. "TR2",
  1967. "HT",
  1968. "FL",
  1969. "SM",
  1970. "SA",
  1971. "BM",
  1972. "SMask",
  1973. "CA",
  1974. "ca",
  1975. "AIS",
  1976. "TK"
  1977. ];
  1978. switch ($action) {
  1979. case "new":
  1980. $this->objects[$id] = ['t' => 'extGState', 'info' => $options];
  1981. // Tell the pages about the new resource
  1982. $this->numStates++;
  1983. $this->o_pages($this->currentNode, 'extGState', ["objNum" => $id, "stateNum" => $this->numStates]);
  1984. break;
  1985. case "out":
  1986. $o = &$this->objects[$id];
  1987. $res = "\n$id 0 obj\n<< /Type /ExtGState\n";
  1988. foreach ($o["info"] as $k => $v) {
  1989. if (!in_array($k, $valid_params)) {
  1990. continue;
  1991. }
  1992. $res .= "/$k $v\n";
  1993. }
  1994. $res .= ">>\nendobj";
  1995. return $res;
  1996. }
  1997. return null;
  1998. }
  1999. /**
  2000. * @param integer $id
  2001. * @param string $action
  2002. * @param mixed $options
  2003. * @return string
  2004. */
  2005. protected function o_xobject($id, $action, $options = '')
  2006. {
  2007. switch ($action) {
  2008. case 'new':
  2009. $this->objects[$id] = ['t' => 'xobject', 'info' => $options, 'c' => ''];
  2010. break;
  2011. case 'procset':
  2012. $this->objects[$id]['procset'] = $options;
  2013. break;
  2014. case 'font':
  2015. $this->objects[$id]['fonts'][$options['fontNum']] = [
  2016. 'objNum' => $options['objNum'],
  2017. 'fontNum' => $options['fontNum']
  2018. ];
  2019. break;
  2020. case 'xObject':
  2021. $this->objects[$id]['xObjects'][] = ['objNum' => $options['objNum'], 'label' => $options['label']];
  2022. break;
  2023. case 'out':
  2024. $o = &$this->objects[$id];
  2025. $res = "\n$id 0 obj\n<< /Type /XObject\n";
  2026. foreach ($o["info"] as $k => $v) {
  2027. switch($k)
  2028. {
  2029. case 'Subtype':
  2030. $res .= "/Subtype /$v\n";
  2031. break;
  2032. case 'bbox':
  2033. $res .= "/BBox [";
  2034. foreach ($v as $value) {
  2035. $res .= sprintf("%.4F ", $value);
  2036. }
  2037. $res .= "]\n";
  2038. break;
  2039. default:
  2040. $res .= "/$k $v\n";
  2041. break;
  2042. }
  2043. }
  2044. $res .= "/Matrix[1.0 0.0 0.0 1.0 0.0 0.0]\n";
  2045. $res .= "/Resources <<";
  2046. if (isset($o['procset'])) {
  2047. $res .= "\n/ProcSet " . $o['procset'] . " 0 R";
  2048. } else {
  2049. $res .= "\n/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]";
  2050. }
  2051. if (isset($o['fonts']) && count($o['fonts'])) {
  2052. $res .= "\n/Font << ";
  2053. foreach ($o['fonts'] as $finfo) {
  2054. $res .= "\n/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R";
  2055. }
  2056. $res .= "\n>>";
  2057. }
  2058. if (isset($o['xObjects']) && count($o['xObjects'])) {
  2059. $res .= "\n/XObject << ";
  2060. foreach ($o['xObjects'] as $finfo) {
  2061. $res .= "\n/" . $finfo['label'] . " " . $finfo['objNum'] . " 0 R";
  2062. }
  2063. $res .= "\n>>";
  2064. }
  2065. $res .= "\n>>\n";
  2066. $tmp = $o["c"];
  2067. if ($this->compressionReady && $this->options['compression']) {
  2068. // then implement ZLIB based compression on this content stream
  2069. $res .= " /Filter /FlateDecode\n";
  2070. $tmp = gzcompress($tmp, 6);
  2071. }
  2072. if ($this->encrypted) {
  2073. $this->encryptInit($id);
  2074. $tmp = $this->ARC4($tmp);
  2075. }
  2076. $res .= "/Length " . mb_strlen($tmp, '8bit') . " >>\n";
  2077. $res .= "stream\n" . $tmp . "\nendstream" . "\nendobj";;
  2078. return $res;
  2079. }
  2080. return null;
  2081. }
  2082. /**
  2083. * @param $id
  2084. * @param $action
  2085. * @param string $options
  2086. * @return null|string
  2087. */
  2088. protected function o_acroform($id, $action, $options = '')
  2089. {
  2090. switch ($action) {
  2091. case "new":
  2092. $this->o_catalog($this->catalogId, 'acroform', $id);
  2093. $this->objects[$id] = array('t' => 'acroform', 'info' => $options);
  2094. break;
  2095. case 'addfield':
  2096. $this->objects[$id]['info']['Fields'][] = $options;
  2097. break;
  2098. case 'font':
  2099. $this->objects[$id]['fonts'][$options['fontNum']] = [
  2100. 'objNum' => $options['objNum'],
  2101. 'fontNum' => $options['fontNum']
  2102. ];
  2103. break;
  2104. case "out":
  2105. $o = &$this->objects[$id];
  2106. $res = "\n$id 0 obj\n<<";
  2107. foreach ($o["info"] as $k => $v) {
  2108. switch($k) {
  2109. case 'Fields':
  2110. $res .= " /Fields [";
  2111. foreach ($v as $i) {
  2112. $res .= "$i 0 R ";
  2113. }
  2114. $res .= "]\n";
  2115. break;
  2116. default:
  2117. $res .= "/$k $v\n";
  2118. }
  2119. }
  2120. $res .= "/DR <<\n";
  2121. if (isset($o['fonts']) && count($o['fonts'])) {
  2122. $res .= "/Font << \n";
  2123. foreach ($o['fonts'] as $finfo) {
  2124. $res .= "/F" . $finfo['fontNum'] . " " . $finfo['objNum'] . " 0 R\n";
  2125. }
  2126. $res .= ">>\n";
  2127. }
  2128. $res .= ">>\n";
  2129. $res .= ">>\nendobj";
  2130. return $res;
  2131. }
  2132. return null;
  2133. }
  2134. /**
  2135. * @param $id
  2136. * @param $action
  2137. * @param mixed $options
  2138. * @return null|string
  2139. */
  2140. protected function o_field($id, $action, $options = '')
  2141. {
  2142. switch ($action) {
  2143. case "new":
  2144. $this->o_page($options['pageid'], 'annot', $id);
  2145. $this->o_acroform($this->acroFormId, 'addfield', $id);
  2146. $this->objects[$id] = ['t' => 'field', 'info' => $options];
  2147. break;
  2148. case 'set':
  2149. $this->objects[$id]['info'] = array_merge($this->objects[$id]['info'], $options);
  2150. break;
  2151. case "out":
  2152. $o = &$this->objects[$id];
  2153. $res = "\n$id 0 obj\n<< /Type /Annot /Subtype /Widget \n";
  2154. $encrypted = $this->encrypted;
  2155. if ($encrypted) {
  2156. $this->encryptInit($id);
  2157. }
  2158. foreach ($o["info"] as $k => $v) {
  2159. switch ($k) {
  2160. case 'pageid':
  2161. $res .= "/P $v 0 R\n";
  2162. break;
  2163. case 'value':
  2164. if ($encrypted) {
  2165. $v = $this->filterText($this->ARC4($v), false, false);
  2166. }
  2167. $res .= "/V ($v)\n";
  2168. break;
  2169. case 'refvalue':
  2170. $res .= "/V $v 0 R\n";
  2171. break;
  2172. case 'da':
  2173. if ($encrypted) {
  2174. $v = $this->filterText($this->ARC4($v), false, false);
  2175. }
  2176. $res .= "/DA ($v)\n";
  2177. break;
  2178. case 'options':
  2179. $res .= "/Opt [\n";
  2180. foreach ($v as $opt) {
  2181. if ($encrypted) {
  2182. $opt = $this->filterText($this->ARC4($opt), false, false);
  2183. }
  2184. $res .= "($opt)\n";
  2185. }
  2186. $res .= "]\n";
  2187. break;
  2188. case 'rect':
  2189. $res .= "/Rect [";
  2190. foreach ($v as $value) {
  2191. $res .= sprintf("%.4F ", $value);
  2192. }
  2193. $res .= "]\n";
  2194. break;
  2195. case 'appearance':
  2196. $res .= "/AP << ";
  2197. foreach ($v as $a => $ref) {
  2198. $res .= "/$a $ref 0 R ";
  2199. }
  2200. $res .= ">>\n";
  2201. break;
  2202. case 'T':
  2203. if($encrypted) {
  2204. $v = $this->filterText($this->ARC4($v), false, false);
  2205. }
  2206. $res .= "/T ($v)\n";
  2207. break;
  2208. default:
  2209. $res .= "/$k $v\n";
  2210. }
  2211. }
  2212. $res .= ">>\nendobj";
  2213. return $res;
  2214. }
  2215. return null;
  2216. }
  2217. /**
  2218. *
  2219. * @param $id
  2220. * @param $action
  2221. * @param string $options
  2222. * @return null|string
  2223. */
  2224. protected function o_sig($id, $action, $options = '')
  2225. {
  2226. $sign_maxlen = $this->signatureMaxLen;
  2227. switch ($action) {
  2228. case "new":
  2229. $this->objects[$id] = array('t' => 'sig', 'info' => $options);
  2230. $this->byteRange[$id] = ['t' => 'sig'];
  2231. break;
  2232. case 'byterange':
  2233. $o = &$this->objects[$id];
  2234. $content =& $options['content'];
  2235. $content_len = strlen($content);
  2236. $pos = strpos($content, sprintf("/ByteRange [ %'.010d", $id));
  2237. $len = strlen('/ByteRange [ ********** ********** ********** ********** ]');
  2238. $rangeStartPos = $pos + $len + 1 + 10; // before '<'
  2239. $content = substr_replace($content, str_pad(sprintf('/ByteRange [ 0 %u %u %u ]', $rangeStartPos, $rangeStartPos + $sign_maxlen + 2, $content_len - 2 - $sign_maxlen - $rangeStartPos ), $len, ' ', STR_PAD_RIGHT), $pos, $len);
  2240. $fuid = uniqid();
  2241. $tmpInput = $this->tmp . "/pkcs7.tmp." . $fuid . '.in';
  2242. $tmpOutput = $this->tmp . "/pkcs7.tmp." . $fuid . '.out';
  2243. if (file_put_contents($tmpInput, substr($content, 0, $rangeStartPos)) === false) {
  2244. throw new \Exception("Unable to write temporary file for signing.");
  2245. }
  2246. if (file_put_contents($tmpInput, substr($content, $rangeStartPos + 2 + $sign_maxlen),
  2247. FILE_APPEND) === false) {
  2248. throw new \Exception("Unable to write temporary file for signing.");
  2249. }
  2250. if (openssl_pkcs7_sign($tmpInput, $tmpOutput,
  2251. $o['info']['SignCert'],
  2252. array($o['info']['PrivKey'], $o['info']['Password']),
  2253. array(), PKCS7_BINARY | PKCS7_DETACHED) === false) {
  2254. throw new \Exception("Failed to prepare signature.");
  2255. }
  2256. $signature = file_get_contents($tmpOutput);
  2257. unlink($tmpInput);
  2258. unlink($tmpOutput);
  2259. $sign = substr($signature, (strpos($signature, "%%EOF\n\n------") + 13));
  2260. list($head, $signature) = explode("\n\n", $sign);
  2261. $signature = base64_decode(trim($signature));
  2262. $signature = current(unpack('H*', $signature));
  2263. $signature = str_pad($signature, $sign_maxlen, '0');
  2264. $siglen = strlen($signature);
  2265. if (strlen($signature) > $sign_maxlen) {
  2266. throw new \Exception("Signature length ($siglen) exceeds the $sign_maxlen limit.");
  2267. }
  2268. $content = substr_replace($content, $signature, $rangeStartPos + 1, $sign_maxlen);
  2269. break;
  2270. case "out":
  2271. $res = "\n$id 0 obj\n<<\n";
  2272. $encrypted = $this->encrypted;
  2273. if ($encrypted) {
  2274. $this->encryptInit($id);
  2275. }
  2276. $res .= "/ByteRange " .sprintf("[ %'.010d ********** ********** ********** ]\n", $id);
  2277. $res .= "/Contents <" . str_pad('', $sign_maxlen, '0') . ">\n";
  2278. $res .= "/Filter/Adobe.PPKLite\n"; //PPKMS \n";
  2279. $res .= "/Type/Sig/SubFilter/adbe.pkcs7.detached \n";
  2280. $date = "D:" . substr_replace(date('YmdHisO'), '\'', -2, 0) . '\'';
  2281. if ($encrypted) {
  2282. $date = $this->ARC4($date);
  2283. }
  2284. $res .= "/M ($date)\n";
  2285. $res .= "/Prop_Build << /App << /Name /DomPDF >> /Filter << /Name /Adobe.PPKLite >> >>\n";
  2286. $o = &$this->objects[$id];
  2287. foreach ($o['info'] as $k => $v) {
  2288. switch($k) {
  2289. case 'Name':
  2290. case 'Location':
  2291. case 'Reason':
  2292. case 'ContactInfo':
  2293. if ($v !== null && $v !== '') {
  2294. $res .= "/$k (" .
  2295. ($encrypted ? $this->filterText($this->ARC4($v), false, false) : $v) . ") \n";
  2296. }
  2297. break;
  2298. }
  2299. }
  2300. $res .= ">>\nendobj";
  2301. return $res;
  2302. }
  2303. return null;
  2304. }
  2305. /**
  2306. * encryption object.
  2307. *
  2308. * @param $id
  2309. * @param $action
  2310. * @param string $options
  2311. * @return string|null
  2312. */
  2313. protected function o_encryption($id, $action, $options = '')
  2314. {
  2315. switch ($action) {
  2316. case 'new':
  2317. // make the new object
  2318. $this->objects[$id] = ['t' => 'encryption', 'info' => $options];
  2319. $this->arc4_objnum = $id;
  2320. break;
  2321. case 'keys':
  2322. // figure out the additional parameters required
  2323. $pad = chr(0x28) . chr(0xBF) . chr(0x4E) . chr(0x5E) . chr(0x4E) . chr(0x75) . chr(0x8A) . chr(0x41)
  2324. . chr(0x64) . chr(0x00) . chr(0x4E) . chr(0x56) . chr(0xFF) . chr(0xFA) . chr(0x01) . chr(0x08)
  2325. . chr(0x2E) . chr(0x2E) . chr(0x00) . chr(0xB6) . chr(0xD0) . chr(0x68) . chr(0x3E) . chr(0x80)
  2326. . chr(0x2F) . chr(0x0C) . chr(0xA9) . chr(0xFE) . chr(0x64) . chr(0x53) . chr(0x69) . chr(0x7A);
  2327. $info = $this->objects[$id]['info'];
  2328. $len = mb_strlen($info['owner'], '8bit');
  2329. if ($len > 32) {
  2330. $owner = substr($info['owner'], 0, 32);
  2331. } else {
  2332. if ($len < 32) {
  2333. $owner = $info['owner'] . substr($pad, 0, 32 - $len);
  2334. } else {
  2335. $owner = $info['owner'];
  2336. }
  2337. }
  2338. $len = mb_strlen($info['user'], '8bit');
  2339. if ($len > 32) {
  2340. $user = substr($info['user'], 0, 32);
  2341. } else {
  2342. if ($len < 32) {
  2343. $user = $info['user'] . substr($pad, 0, 32 - $len);
  2344. } else {
  2345. $user = $info['user'];
  2346. }
  2347. }
  2348. $tmp = $this->md5_16($owner);
  2349. $okey = substr($tmp, 0, 5);
  2350. $this->ARC4_init($okey);
  2351. $ovalue = $this->ARC4($user);
  2352. $this->objects[$id]['info']['O'] = $ovalue;
  2353. // now make the u value, phew.
  2354. $tmp = $this->md5_16(
  2355. $user . $ovalue . chr($info['p']) . chr(255) . chr(255) . chr(255) . hex2bin($this->fileIdentifier)
  2356. );
  2357. $ukey = substr($tmp, 0, 5);
  2358. $this->ARC4_init($ukey);
  2359. $this->encryptionKey = $ukey;
  2360. $this->encrypted = true;
  2361. $uvalue = $this->ARC4($pad);
  2362. $this->objects[$id]['info']['U'] = $uvalue;
  2363. // initialize the arc4 array
  2364. break;
  2365. case 'out':
  2366. $o = &$this->objects[$id];
  2367. $res = "\n$id 0 obj\n<<";
  2368. $res .= "\n/Filter /Standard";
  2369. $res .= "\n/V 1";
  2370. $res .= "\n/R 2";
  2371. $res .= "\n/O (" . $this->filterText($o['info']['O'], false, false) . ')';
  2372. $res .= "\n/U (" . $this->filterText($o['info']['U'], false, false) . ')';
  2373. // and the p-value needs to be converted to account for the twos-complement approach
  2374. $o['info']['p'] = (($o['info']['p'] ^ 255) + 1) * -1;
  2375. $res .= "\n/P " . ($o['info']['p']);
  2376. $res .= "\n>>\nendobj";
  2377. return $res;
  2378. }
  2379. return null;
  2380. }
  2381. protected function o_indirect_references($id, $action, $options = null)
  2382. {
  2383. switch ($action) {
  2384. case 'new':
  2385. case 'add':
  2386. if ($id === 0) {
  2387. $id = ++$this->numObj;
  2388. $this->o_catalog($this->catalogId, 'names', $id);
  2389. $this->objects[$id] = ['t' => 'indirect_references', 'info' => $options];
  2390. $this->indirectReferenceId = $id;
  2391. } else {
  2392. $this->objects[$id]['info'] = array_merge($this->objects[$id]['info'], $options);
  2393. }
  2394. break;
  2395. case 'out':
  2396. $res = "\n$id 0 obj << ";
  2397. foreach($this->objects[$id]['info'] as $referenceObjName => $referenceObjId) {
  2398. $res .= "/$referenceObjName $referenceObjId 0 R ";
  2399. }
  2400. $res .= ">> endobj";
  2401. return $res;
  2402. }
  2403. return null;
  2404. }
  2405. protected function o_names($id, $action, $options = null)
  2406. {
  2407. switch ($action) {
  2408. case 'new':
  2409. case 'add':
  2410. if ($id === 0) {
  2411. $id = ++$this->numObj;
  2412. $this->objects[$id] = ['t' => 'names', 'info' => [$options]];
  2413. $this->o_indirect_references($this->indirectReferenceId, 'add', ['EmbeddedFiles' => $id]);
  2414. $this->embeddedFilesId = $id;
  2415. } else {
  2416. $this->objects[$id]['info'][] = $options;
  2417. }
  2418. break;
  2419. case 'out':
  2420. $info = &$this->objects[$id]['info'];
  2421. $res = '';
  2422. if (count($info) > 0) {
  2423. $res = "\n$id 0 obj << /Names [ ";
  2424. if ($this->encrypted) {
  2425. $this->encryptInit($id);
  2426. }
  2427. foreach ($info as $entry) {
  2428. if ($this->encrypted) {
  2429. $filename = $this->ARC4($entry['filename']);
  2430. } else {
  2431. $filename = $entry['filename'];
  2432. }
  2433. $res .= "($filename) " . $entry['dict_reference'] . " 0 R ";
  2434. }
  2435. $res .= "] >> endobj";
  2436. }
  2437. return $res;
  2438. }
  2439. return null;
  2440. }
  2441. protected function o_embedded_file_dictionary($id, $action, $options = null)
  2442. {
  2443. switch ($action) {
  2444. case 'new':
  2445. $embeddedFileId = ++$this->numObj;
  2446. $options['embedded_reference'] = $embeddedFileId;
  2447. $this->objects[$id] = ['t' => 'embedded_file_dictionary', 'info' => $options];
  2448. $this->o_embedded_file($embeddedFileId, 'new', $options);
  2449. $options['dict_reference'] = $id;
  2450. $this->o_names($this->embeddedFilesId, 'add', $options);
  2451. break;
  2452. case 'out':
  2453. $info = &$this->objects[$id]['info'];
  2454. if ($this->encrypted) {
  2455. $this->encryptInit($id);
  2456. $filename = $this->ARC4($info['filename']);
  2457. $description = $this->ARC4($info['description']);
  2458. } else {
  2459. $filename = $info['filename'];
  2460. $description = $info['description'];
  2461. }
  2462. $res = "\n$id 0 obj <</Type /Filespec /EF";
  2463. $res .= " <</F " . $info['embedded_reference'] . " 0 R >>";
  2464. $res .= " /F ($filename) /UF ($filename) /Desc ($description)";
  2465. $res .= " >> endobj";
  2466. return $res;
  2467. }
  2468. return null;
  2469. }
  2470. protected function o_embedded_file($id, $action, $options = null): ?string
  2471. {
  2472. switch ($action) {
  2473. case 'new':
  2474. $this->objects[$id] = ['t' => 'embedded_file', 'info' => $options];
  2475. break;
  2476. case 'out':
  2477. $info = &$this->objects[$id]['info'];
  2478. if ($this->compressionReady) {
  2479. $filepath = $info['filepath'];
  2480. $checksum = md5_file($filepath);
  2481. $f = fopen($filepath, "rb");
  2482. $file_content_compressed = '';
  2483. $deflateContext = deflate_init(ZLIB_ENCODING_DEFLATE, ['level' => 6]);
  2484. while (($block = fread($f, 8192))) {
  2485. $file_content_compressed .= deflate_add($deflateContext, $block, ZLIB_NO_FLUSH);
  2486. }
  2487. $file_content_compressed .= deflate_add($deflateContext, '', ZLIB_FINISH);
  2488. $file_size_uncompressed = ftell($f);
  2489. fclose($f);
  2490. } else {
  2491. $file_content = file_get_contents($info['filepath']);
  2492. $file_size_uncompressed = mb_strlen($file_content, '8bit');
  2493. $checksum = md5($file_content);
  2494. }
  2495. if ($this->encrypted) {
  2496. $this->encryptInit($id);
  2497. $checksum = $this->ARC4($checksum);
  2498. $file_content_compressed = $this->ARC4($file_content_compressed);
  2499. }
  2500. $file_size_compressed = mb_strlen($file_content_compressed, '8bit');
  2501. $res = "\n$id 0 obj <</Params <</Size $file_size_uncompressed /CheckSum ($checksum) >>" .
  2502. " /Type/EmbeddedFile /Filter/FlateDecode" .
  2503. " /Length $file_size_compressed >> stream\n$file_content_compressed\nendstream\nendobj";
  2504. return $res;
  2505. }
  2506. return null;
  2507. }
  2508. /**
  2509. * ARC4 functions
  2510. * A series of function to implement ARC4 encoding in PHP
  2511. */
  2512. /**
  2513. * calculate the 16 byte version of the 128 bit md5 digest of the string
  2514. *
  2515. * @param $string
  2516. * @return string
  2517. */
  2518. function md5_16($string)
  2519. {
  2520. $tmp = md5($string);
  2521. $out = '';
  2522. for ($i = 0; $i <= 30; $i = $i + 2) {
  2523. $out .= chr(hexdec(substr($tmp, $i, 2)));
  2524. }
  2525. return $out;
  2526. }
  2527. /**
  2528. * initialize the encryption for processing a particular object
  2529. *
  2530. * @param $id
  2531. */
  2532. function encryptInit($id)
  2533. {
  2534. $tmp = $this->encryptionKey;
  2535. $hex = dechex($id);
  2536. if (mb_strlen($hex, '8bit') < 6) {
  2537. $hex = substr('000000', 0, 6 - mb_strlen($hex, '8bit')) . $hex;
  2538. }
  2539. $tmp .= chr(hexdec(substr($hex, 4, 2)))
  2540. . chr(hexdec(substr($hex, 2, 2)))
  2541. . chr(hexdec(substr($hex, 0, 2)))
  2542. . chr(0)
  2543. . chr(0)
  2544. ;
  2545. $key = $this->md5_16($tmp);
  2546. $this->ARC4_init(substr($key, 0, 10));
  2547. }
  2548. /**
  2549. * initialize the ARC4 encryption
  2550. *
  2551. * @param string $key
  2552. */
  2553. function ARC4_init($key = '')
  2554. {
  2555. $this->arc4 = '';
  2556. // setup the control array
  2557. if (mb_strlen($key, '8bit') == 0) {
  2558. return;
  2559. }
  2560. $k = '';
  2561. while (mb_strlen($k, '8bit') < 256) {
  2562. $k .= $key;
  2563. }
  2564. $k = substr($k, 0, 256);
  2565. for ($i = 0; $i < 256; $i++) {
  2566. $this->arc4 .= chr($i);
  2567. }
  2568. $j = 0;
  2569. for ($i = 0; $i < 256; $i++) {
  2570. $t = $this->arc4[$i];
  2571. $j = ($j + ord($t) + ord($k[$i])) % 256;
  2572. $this->arc4[$i] = $this->arc4[$j];
  2573. $this->arc4[$j] = $t;
  2574. }
  2575. }
  2576. /**
  2577. * ARC4 encrypt a text string
  2578. *
  2579. * @param $text
  2580. * @return string
  2581. */
  2582. function ARC4($text)
  2583. {
  2584. $len = mb_strlen($text, '8bit');
  2585. $a = 0;
  2586. $b = 0;
  2587. $c = $this->arc4;
  2588. $out = '';
  2589. for ($i = 0; $i < $len; $i++) {
  2590. $a = ($a + 1) % 256;
  2591. $t = $c[$a];
  2592. $b = ($b + ord($t)) % 256;
  2593. $c[$a] = $c[$b];
  2594. $c[$b] = $t;
  2595. $k = ord($c[(ord($c[$a]) + ord($c[$b])) % 256]);
  2596. $out .= chr(ord($text[$i]) ^ $k);
  2597. }
  2598. return $out;
  2599. }
  2600. /**
  2601. * functions which can be called to adjust or add to the document
  2602. */
  2603. /**
  2604. * add a link in the document to an external URL
  2605. *
  2606. * @param $url
  2607. * @param $x0
  2608. * @param $y0
  2609. * @param $x1
  2610. * @param $y1
  2611. */
  2612. function addLink($url, $x0, $y0, $x1, $y1)
  2613. {
  2614. $this->numObj++;
  2615. $info = ['type' => 'link', 'url' => $url, 'rect' => [$x0, $y0, $x1, $y1]];
  2616. $this->o_annotation($this->numObj, 'new', $info);
  2617. }
  2618. /**
  2619. * add a link in the document to an internal destination (ie. within the document)
  2620. *
  2621. * @param $label
  2622. * @param $x0
  2623. * @param $y0
  2624. * @param $x1
  2625. * @param $y1
  2626. */
  2627. function addInternalLink($label, $x0, $y0, $x1, $y1)
  2628. {
  2629. $this->numObj++;
  2630. $info = ['type' => 'ilink', 'label' => $label, 'rect' => [$x0, $y0, $x1, $y1]];
  2631. $this->o_annotation($this->numObj, 'new', $info);
  2632. }
  2633. /**
  2634. * set the encryption of the document
  2635. * can be used to turn it on and/or set the passwords which it will have.
  2636. * also the functions that the user will have are set here, such as print, modify, add
  2637. *
  2638. * @param string $userPass
  2639. * @param string $ownerPass
  2640. * @param array $pc
  2641. */
  2642. function setEncryption($userPass = '', $ownerPass = '', $pc = [])
  2643. {
  2644. $p = bindec("11000000");
  2645. $options = ['print' => 4, 'modify' => 8, 'copy' => 16, 'add' => 32];
  2646. foreach ($pc as $k => $v) {
  2647. if ($v && isset($options[$k])) {
  2648. $p += $options[$k];
  2649. } else {
  2650. if (isset($options[$v])) {
  2651. $p += $options[$v];
  2652. }
  2653. }
  2654. }
  2655. // implement encryption on the document
  2656. if ($this->arc4_objnum == 0) {
  2657. // then the block does not exist already, add it.
  2658. $this->numObj++;
  2659. if (mb_strlen($ownerPass) == 0) {
  2660. $ownerPass = $userPass;
  2661. }
  2662. $this->o_encryption($this->numObj, 'new', ['user' => $userPass, 'owner' => $ownerPass, 'p' => $p]);
  2663. }
  2664. }
  2665. /**
  2666. * should be used for internal checks, not implemented as yet
  2667. */
  2668. function checkAllHere()
  2669. {
  2670. }
  2671. /**
  2672. * return the pdf stream as a string returned from the function
  2673. *
  2674. * @param bool $debug
  2675. * @return string
  2676. */
  2677. function output($debug = false)
  2678. {
  2679. if ($debug) {
  2680. // turn compression off
  2681. $this->options['compression'] = false;
  2682. }
  2683. if ($this->javascript) {
  2684. $this->numObj++;
  2685. $js_id = $this->numObj;
  2686. $this->o_embedjs($js_id, 'new');
  2687. $this->o_javascript(++$this->numObj, 'new', $this->javascript);
  2688. $id = $this->catalogId;
  2689. $this->o_indirect_references($this->indirectReferenceId, 'add', ['JavaScript' => $js_id]);
  2690. }
  2691. if ($this->fileIdentifier === '') {
  2692. $tmp = implode('', $this->objects[$this->infoObject]['info']);
  2693. $this->fileIdentifier = md5('DOMPDF' . __FILE__ . $tmp . microtime() . mt_rand());
  2694. }
  2695. if ($this->arc4_objnum) {
  2696. $this->o_encryption($this->arc4_objnum, 'keys');
  2697. $this->ARC4_init($this->encryptionKey);
  2698. }
  2699. $this->checkAllHere();
  2700. $xref = [];
  2701. $content = '%PDF-' . self::PDF_VERSION;
  2702. $pos = mb_strlen($content, '8bit');
  2703. // pre-process o_font objects before output of all objects
  2704. foreach ($this->objects as $k => $v) {
  2705. if ($v['t'] === 'font') {
  2706. $this->o_font($k, 'add');
  2707. }
  2708. }
  2709. foreach ($this->objects as $k => $v) {
  2710. $tmp = 'o_' . $v['t'];
  2711. $cont = $this->$tmp($k, 'out');
  2712. $content .= $cont;
  2713. $xref[] = $pos + 1; //+1 to account for \n at the start of each object
  2714. $pos += mb_strlen($cont, '8bit');
  2715. }
  2716. $content .= "\nxref\n0 " . (count($xref) + 1) . "\n0000000000 65535 f \n";
  2717. foreach ($xref as $p) {
  2718. $content .= str_pad($p, 10, "0", STR_PAD_LEFT) . " 00000 n \n";
  2719. }
  2720. $content .= "trailer\n<<\n" .
  2721. '/Size ' . (count($xref) + 1) . "\n" .
  2722. '/Root 1 0 R' . "\n" .
  2723. '/Info ' . $this->infoObject . " 0 R\n"
  2724. ;
  2725. // if encryption has been applied to this document then add the marker for this dictionary
  2726. if ($this->arc4_objnum > 0) {
  2727. $content .= '/Encrypt ' . $this->arc4_objnum . " 0 R\n";
  2728. }
  2729. $content .= '/ID[<' . $this->fileIdentifier . '><' . $this->fileIdentifier . ">]\n";
  2730. // account for \n added at start of xref table
  2731. $pos++;
  2732. $content .= ">>\nstartxref\n$pos\n%%EOF\n";
  2733. if (count($this->byteRange) > 0) {
  2734. foreach ($this->byteRange as $k => $v) {
  2735. $tmp = 'o_' . $v['t'];
  2736. $this->$tmp($k, 'byterange', ['content' => &$content]);
  2737. }
  2738. }
  2739. return $content;
  2740. }
  2741. /**
  2742. * initialize a new document
  2743. * if this is called on an existing document results may be unpredictable, but the existing document would be lost at minimum
  2744. * this function is called automatically by the constructor function
  2745. *
  2746. * @param array $pageSize
  2747. */
  2748. private function newDocument($pageSize = [0, 0, 612, 792])
  2749. {
  2750. $this->numObj = 0;
  2751. $this->objects = [];
  2752. $this->numObj++;
  2753. $this->o_catalog($this->numObj, 'new');
  2754. $this->numObj++;
  2755. $this->o_outlines($this->numObj, 'new');
  2756. $this->numObj++;
  2757. $this->o_pages($this->numObj, 'new');
  2758. $this->o_pages($this->numObj, 'mediaBox', $pageSize);
  2759. $this->currentNode = 3;
  2760. $this->numObj++;
  2761. $this->o_procset($this->numObj, 'new');
  2762. $this->numObj++;
  2763. $this->o_info($this->numObj, 'new');
  2764. $this->numObj++;
  2765. $this->o_page($this->numObj, 'new');
  2766. // need to store the first page id as there is no way to get it to the user during
  2767. // startup
  2768. $this->firstPageId = $this->currentContents;
  2769. }
  2770. /**
  2771. * open the font file and return a php structure containing it.
  2772. * first check if this one has been done before and saved in a form more suited to php
  2773. * note that if a php serialized version does not exist it will try and make one, but will
  2774. * require write access to the directory to do it... it is MUCH faster to have these serialized
  2775. * files.
  2776. *
  2777. * @param $font
  2778. */
  2779. private function openFont($font)
  2780. {
  2781. // assume that $font contains the path and file but not the extension
  2782. $name = basename($font);
  2783. $dir = dirname($font) . '/';
  2784. $fontcache = $this->fontcache;
  2785. if ($fontcache == '') {
  2786. $fontcache = rtrim($dir, DIRECTORY_SEPARATOR."/\\");
  2787. }
  2788. //$name filename without folder and extension of font metrics
  2789. //$dir folder of font metrics
  2790. //$fontcache folder of runtime created php serialized version of font metrics.
  2791. // If this is not given, the same folder as the font metrics will be used.
  2792. // Storing and reusing serialized versions improves speed much
  2793. $this->addMessage("openFont: $font - $name");
  2794. if (!$this->isUnicode || in_array(mb_strtolower(basename($name)), self::$coreFonts)) {
  2795. $metrics_name = "$name.afm";
  2796. } else {
  2797. $metrics_name = "$name.ufm";
  2798. }
  2799. $cache_name = "$metrics_name.php";
  2800. $this->addMessage("metrics: $metrics_name, cache: $cache_name");
  2801. if (file_exists($fontcache . '/' . $cache_name)) {
  2802. $this->addMessage("openFont: php file exists $fontcache/$cache_name");
  2803. $this->fonts[$font] = require($fontcache . '/' . $cache_name);
  2804. if (!isset($this->fonts[$font]['_version_']) || $this->fonts[$font]['_version_'] != $this->fontcacheVersion) {
  2805. // if the font file is old, then clear it out and prepare for re-creation
  2806. $this->addMessage('openFont: clear out, make way for new version.');
  2807. $this->fonts[$font] = null;
  2808. unset($this->fonts[$font]);
  2809. }
  2810. } else {
  2811. $old_cache_name = "php_$metrics_name";
  2812. if (file_exists($fontcache . '/' . $old_cache_name)) {
  2813. $this->addMessage(
  2814. "openFont: php file doesn't exist $fontcache/$cache_name, creating it from the old format"
  2815. );
  2816. $old_cache = file_get_contents($fontcache . '/' . $old_cache_name);
  2817. file_put_contents($fontcache . '/' . $cache_name, '<?php return ' . $old_cache . ';');
  2818. $this->openFont($font);
  2819. return;
  2820. }
  2821. }
  2822. if (!isset($this->fonts[$font]) && file_exists($dir . $metrics_name)) {
  2823. // then rebuild the php_<font>.afm file from the <font>.afm file
  2824. $this->addMessage("openFont: build php file from $dir$metrics_name");
  2825. $data = [];
  2826. // 20 => 'space'
  2827. $data['codeToName'] = [];
  2828. // Since we're not going to enable Unicode for the core fonts we need to use a font-based
  2829. // setting for Unicode support rather than a global setting.
  2830. $data['isUnicode'] = (strtolower(substr($metrics_name, -3)) !== 'afm');
  2831. $cidtogid = '';
  2832. if ($data['isUnicode']) {
  2833. $cidtogid = str_pad('', 256 * 256 * 2, "\x00");
  2834. }
  2835. $file = file($dir . $metrics_name);
  2836. foreach ($file as $rowA) {
  2837. $row = trim($rowA);
  2838. $pos = strpos($row, ' ');
  2839. if ($pos) {
  2840. // then there must be some keyword
  2841. $key = substr($row, 0, $pos);
  2842. switch ($key) {
  2843. case 'FontName':
  2844. case 'FullName':
  2845. case 'FamilyName':
  2846. case 'PostScriptName':
  2847. case 'Weight':
  2848. case 'ItalicAngle':
  2849. case 'IsFixedPitch':
  2850. case 'CharacterSet':
  2851. case 'UnderlinePosition':
  2852. case 'UnderlineThickness':
  2853. case 'Version':
  2854. case 'EncodingScheme':
  2855. case 'CapHeight':
  2856. case 'XHeight':
  2857. case 'Ascender':
  2858. case 'Descender':
  2859. case 'StdHW':
  2860. case 'StdVW':
  2861. case 'StartCharMetrics':
  2862. case 'FontHeightOffset': // OAR - Added so we can offset the height calculation of a Windows font. Otherwise it's too big.
  2863. $data[$key] = trim(substr($row, $pos));
  2864. break;
  2865. case 'FontBBox':
  2866. $data[$key] = explode(' ', trim(substr($row, $pos)));
  2867. break;
  2868. //C 39 ; WX 222 ; N quoteright ; B 53 463 157 718 ;
  2869. case 'C': // Found in AFM files
  2870. $bits = explode(';', trim($row));
  2871. $dtmp = ['C' => null, 'N' => null, 'WX' => null, 'B' => []];
  2872. foreach ($bits as $bit) {
  2873. $bits2 = explode(' ', trim($bit));
  2874. if (mb_strlen($bits2[0], '8bit') == 0) {
  2875. continue;
  2876. }
  2877. if (count($bits2) > 2) {
  2878. $dtmp[$bits2[0]] = [];
  2879. for ($i = 1; $i < count($bits2); $i++) {
  2880. $dtmp[$bits2[0]][] = $bits2[$i];
  2881. }
  2882. } else {
  2883. if (count($bits2) == 2) {
  2884. $dtmp[$bits2[0]] = $bits2[1];
  2885. }
  2886. }
  2887. }
  2888. $c = (int)$dtmp['C'];
  2889. $n = $dtmp['N'];
  2890. $width = floatval($dtmp['WX']);
  2891. if ($c >= 0) {
  2892. if (!ctype_xdigit($n) || $c != hexdec($n)) {
  2893. $data['codeToName'][$c] = $n;
  2894. }
  2895. $data['C'][$c] = $width;
  2896. } elseif (isset($n)) {
  2897. $data['C'][$n] = $width;
  2898. }
  2899. if (!isset($data['MissingWidth']) && $c === -1 && $n === '.notdef') {
  2900. $data['MissingWidth'] = $width;
  2901. }
  2902. break;
  2903. // U 827 ; WX 0 ; N squaresubnosp ; G 675 ;
  2904. case 'U': // Found in UFM files
  2905. if (!$data['isUnicode']) {
  2906. break;
  2907. }
  2908. $bits = explode(';', trim($row));
  2909. $dtmp = ['G' => null, 'N' => null, 'U' => null, 'WX' => null];
  2910. foreach ($bits as $bit) {
  2911. $bits2 = explode(' ', trim($bit));
  2912. if (mb_strlen($bits2[0], '8bit') === 0) {
  2913. continue;
  2914. }
  2915. if (count($bits2) > 2) {
  2916. $dtmp[$bits2[0]] = [];
  2917. for ($i = 1; $i < count($bits2); $i++) {
  2918. $dtmp[$bits2[0]][] = $bits2[$i];
  2919. }
  2920. } else {
  2921. if (count($bits2) == 2) {
  2922. $dtmp[$bits2[0]] = $bits2[1];
  2923. }
  2924. }
  2925. }
  2926. $c = (int)$dtmp['U'];
  2927. $n = $dtmp['N'];
  2928. $glyph = $dtmp['G'];
  2929. $width = floatval($dtmp['WX']);
  2930. if ($c >= 0) {
  2931. // Set values in CID to GID map
  2932. if ($c >= 0 && $c < 0xFFFF && $glyph) {
  2933. $cidtogid[$c * 2] = chr($glyph >> 8);
  2934. $cidtogid[$c * 2 + 1] = chr($glyph & 0xFF);
  2935. }
  2936. if (!ctype_xdigit($n) || $c != hexdec($n)) {
  2937. $data['codeToName'][$c] = $n;
  2938. }
  2939. $data['C'][$c] = $width;
  2940. } elseif (isset($n)) {
  2941. $data['C'][$n] = $width;
  2942. }
  2943. if (!isset($data['MissingWidth']) && $c === -1 && $n === '.notdef') {
  2944. $data['MissingWidth'] = $width;
  2945. }
  2946. break;
  2947. case 'KPX':
  2948. break; // don't include them as they are not used yet
  2949. //KPX Adieresis yacute -40
  2950. /*$bits = explode(' ', trim($row));
  2951. $data['KPX'][$bits[1]][$bits[2]] = $bits[3];
  2952. break;*/
  2953. }
  2954. }
  2955. }
  2956. if ($this->compressionReady && $this->options['compression']) {
  2957. // then implement ZLIB based compression on CIDtoGID string
  2958. $data['CIDtoGID_Compressed'] = true;
  2959. $cidtogid = gzcompress($cidtogid, 6);
  2960. }
  2961. $data['CIDtoGID'] = base64_encode($cidtogid);
  2962. $data['_version_'] = $this->fontcacheVersion;
  2963. $this->fonts[$font] = $data;
  2964. //Because of potential trouble with php safe mode, expect that the folder already exists.
  2965. //If not existing, this will hit performance because of missing cached results.
  2966. if (is_dir($fontcache) && is_writable($fontcache)) {
  2967. file_put_contents($fontcache . '/' . $cache_name, '<?php return ' . var_export($data, true) . ';');
  2968. }
  2969. $data = null;
  2970. }
  2971. if (!isset($this->fonts[$font])) {
  2972. $this->addMessage("openFont: no font file found for $font. Do you need to run load_font.php?");
  2973. }
  2974. //pre_r($this->messages);
  2975. }
  2976. /**
  2977. * if the font is not loaded then load it and make the required object
  2978. * else just make it the current font
  2979. * the encoding array can contain 'encoding'=> 'none','WinAnsiEncoding','MacRomanEncoding' or 'MacExpertEncoding'
  2980. * note that encoding='none' will need to be used for symbolic fonts
  2981. * and 'differences' => an array of mappings between numbers 0->255 and character names.
  2982. *
  2983. * @param $fontName
  2984. * @param string $encoding
  2985. * @param bool $set
  2986. * @param bool $isSubsetting
  2987. * @return int
  2988. * @throws FontNotFoundException
  2989. */
  2990. function selectFont($fontName, $encoding = '', $set = true, $isSubsetting = true)
  2991. {
  2992. $ext = substr($fontName, -4);
  2993. if ($ext === '.afm' || $ext === '.ufm') {
  2994. $fontName = substr($fontName, 0, mb_strlen($fontName) - 4);
  2995. }
  2996. if (!isset($this->fonts[$fontName])) {
  2997. $this->addMessage("selectFont: selecting - $fontName - $encoding, $set");
  2998. // load the file
  2999. $this->openFont($fontName);
  3000. if (isset($this->fonts[$fontName])) {
  3001. $this->numObj++;
  3002. $this->numFonts++;
  3003. $font = &$this->fonts[$fontName];
  3004. $name = basename($fontName);
  3005. $options = ['name' => $name, 'fontFileName' => $fontName, 'isSubsetting' => $isSubsetting];
  3006. if (is_array($encoding)) {
  3007. // then encoding and differences might be set
  3008. if (isset($encoding['encoding'])) {
  3009. $options['encoding'] = $encoding['encoding'];
  3010. }
  3011. if (isset($encoding['differences'])) {
  3012. $options['differences'] = $encoding['differences'];
  3013. }
  3014. } else {
  3015. if (mb_strlen($encoding, '8bit')) {
  3016. // then perhaps only the encoding has been set
  3017. $options['encoding'] = $encoding;
  3018. }
  3019. }
  3020. $this->o_font($this->numObj, 'new', $options);
  3021. if (file_exists("$fontName.ttf")) {
  3022. $fileSuffix = 'ttf';
  3023. } elseif (file_exists("$fontName.TTF")) {
  3024. $fileSuffix = 'TTF';
  3025. } elseif (file_exists("$fontName.pfb")) {
  3026. $fileSuffix = 'pfb';
  3027. } elseif (file_exists("$fontName.PFB")) {
  3028. $fileSuffix = 'PFB';
  3029. } else {
  3030. $fileSuffix = '';
  3031. }
  3032. $font['fileSuffix'] = $fileSuffix;
  3033. $font['fontNum'] = $this->numFonts;
  3034. $font['isSubsetting'] = $isSubsetting && $font['isUnicode'] && strtolower($fileSuffix) === 'ttf';
  3035. // also set the differences here, note that this means that these will take effect only the
  3036. //first time that a font is selected, else they are ignored
  3037. if (isset($options['differences'])) {
  3038. $font['differences'] = $options['differences'];
  3039. }
  3040. }
  3041. }
  3042. if ($set && isset($this->fonts[$fontName])) {
  3043. // so if for some reason the font was not set in the last one then it will not be selected
  3044. $this->currentBaseFont = $fontName;
  3045. // the next lines mean that if a new font is selected, then the current text state will be
  3046. // applied to it as well.
  3047. $this->currentFont = $this->currentBaseFont;
  3048. $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum'];
  3049. }
  3050. return $this->currentFontNum;
  3051. }
  3052. /**
  3053. * sets up the current font, based on the font families, and the current text state
  3054. * note that this system is quite flexible, a bold-italic font can be completely different to a
  3055. * italic-bold font, and even bold-bold will have to be defined within the family to have meaning
  3056. * This function is to be called whenever the currentTextState is changed, it will update
  3057. * the currentFont setting to whatever the appropriate family one is.
  3058. * If the user calls selectFont themselves then that will reset the currentBaseFont, and the currentFont
  3059. * This function will change the currentFont to whatever it should be, but will not change the
  3060. * currentBaseFont.
  3061. */
  3062. private function setCurrentFont()
  3063. {
  3064. // if (strlen($this->currentBaseFont) == 0){
  3065. // // then assume an initial font
  3066. // $this->selectFont($this->defaultFont);
  3067. // }
  3068. // $cf = substr($this->currentBaseFont,strrpos($this->currentBaseFont,'/')+1);
  3069. // if (strlen($this->currentTextState)
  3070. // && isset($this->fontFamilies[$cf])
  3071. // && isset($this->fontFamilies[$cf][$this->currentTextState])){
  3072. // // then we are in some state or another
  3073. // // and this font has a family, and the current setting exists within it
  3074. // // select the font, then return it
  3075. // $nf = substr($this->currentBaseFont,0,strrpos($this->currentBaseFont,'/')+1).$this->fontFamilies[$cf][$this->currentTextState];
  3076. // $this->selectFont($nf,'',0);
  3077. // $this->currentFont = $nf;
  3078. // $this->currentFontNum = $this->fonts[$nf]['fontNum'];
  3079. // } else {
  3080. // // the this font must not have the right family member for the current state
  3081. // // simply assume the base font
  3082. $this->currentFont = $this->currentBaseFont;
  3083. $this->currentFontNum = $this->fonts[$this->currentFont]['fontNum'];
  3084. // }
  3085. }
  3086. /**
  3087. * function for the user to find out what the ID is of the first page that was created during
  3088. * startup - useful if they wish to add something to it later.
  3089. *
  3090. * @return int
  3091. */
  3092. function getFirstPageId()
  3093. {
  3094. return $this->firstPageId;
  3095. }
  3096. /**
  3097. * add content to the currently active object
  3098. *
  3099. * @param $content
  3100. */
  3101. private function addContent($content)
  3102. {
  3103. $this->objects[$this->currentContents]['c'] .= $content;
  3104. }
  3105. /**
  3106. * sets the color for fill operations
  3107. *
  3108. * @param $color
  3109. * @param bool $force
  3110. */
  3111. function setColor($color, $force = false)
  3112. {
  3113. $new_color = [$color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null];
  3114. if (!$force && $this->currentColor == $new_color) {
  3115. return;
  3116. }
  3117. if (isset($new_color[3])) {
  3118. $this->currentColor = $new_color;
  3119. $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F k", $this->currentColor));
  3120. } else {
  3121. if (isset($new_color[2])) {
  3122. $this->currentColor = $new_color;
  3123. $this->addContent(vsprintf("\n%.3F %.3F %.3F rg", $this->currentColor));
  3124. }
  3125. }
  3126. }
  3127. /**
  3128. * sets the color for fill operations
  3129. *
  3130. * @param $fillRule
  3131. */
  3132. function setFillRule($fillRule)
  3133. {
  3134. if (!in_array($fillRule, ["nonzero", "evenodd"])) {
  3135. return;
  3136. }
  3137. $this->fillRule = $fillRule;
  3138. }
  3139. /**
  3140. * sets the color for stroke operations
  3141. *
  3142. * @param $color
  3143. * @param bool $force
  3144. */
  3145. function setStrokeColor($color, $force = false)
  3146. {
  3147. $new_color = [$color[0], $color[1], $color[2], isset($color[3]) ? $color[3] : null];
  3148. if (!$force && $this->currentStrokeColor == $new_color) {
  3149. return;
  3150. }
  3151. if (isset($new_color[3])) {
  3152. $this->currentStrokeColor = $new_color;
  3153. $this->addContent(vsprintf("\n%.3F %.3F %.3F %.3F K", $this->currentStrokeColor));
  3154. } else {
  3155. if (isset($new_color[2])) {
  3156. $this->currentStrokeColor = $new_color;
  3157. $this->addContent(vsprintf("\n%.3F %.3F %.3F RG", $this->currentStrokeColor));
  3158. }
  3159. }
  3160. }
  3161. /**
  3162. * Set the graphics state for compositions
  3163. *
  3164. * @param $parameters
  3165. */
  3166. function setGraphicsState($parameters)
  3167. {
  3168. // Create a new graphics state object if necessary
  3169. if (($gstate = array_search($parameters, $this->gstates)) === false) {
  3170. $this->numObj++;
  3171. $this->o_extGState($this->numObj, 'new', $parameters);
  3172. $gstate = $this->numStates;
  3173. $this->gstates[$gstate] = $parameters;
  3174. }
  3175. $this->addContent("\n/GS$gstate gs");
  3176. }
  3177. /**
  3178. * Set current blend mode & opacity for lines.
  3179. *
  3180. * Valid blend modes are:
  3181. *
  3182. * Normal, Multiply, Screen, Overlay, Darken, Lighten,
  3183. * ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
  3184. * Exclusion
  3185. *
  3186. * @param string $mode the blend mode to use
  3187. * @param float $opacity 0.0 fully transparent, 1.0 fully opaque
  3188. */
  3189. function setLineTransparency($mode, $opacity)
  3190. {
  3191. static $blend_modes = [
  3192. "Normal",
  3193. "Multiply",
  3194. "Screen",
  3195. "Overlay",
  3196. "Darken",
  3197. "Lighten",
  3198. "ColorDogde",
  3199. "ColorBurn",
  3200. "HardLight",
  3201. "SoftLight",
  3202. "Difference",
  3203. "Exclusion"
  3204. ];
  3205. if (!in_array($mode, $blend_modes)) {
  3206. $mode = "Normal";
  3207. }
  3208. // Only create a new graphics state if required
  3209. if ($mode === $this->currentLineTransparency["mode"] &&
  3210. $opacity == $this->currentLineTransparency["opacity"]
  3211. ) {
  3212. return;
  3213. }
  3214. $this->currentLineTransparency["mode"] = $mode;
  3215. $this->currentLineTransparency["opacity"] = $opacity;
  3216. $options = [
  3217. "BM" => "/$mode",
  3218. "CA" => (float)$opacity
  3219. ];
  3220. $this->setGraphicsState($options);
  3221. }
  3222. /**
  3223. * Set current blend mode & opacity for filled objects.
  3224. *
  3225. * Valid blend modes are:
  3226. *
  3227. * Normal, Multiply, Screen, Overlay, Darken, Lighten,
  3228. * ColorDogde, ColorBurn, HardLight, SoftLight, Difference,
  3229. * Exclusion
  3230. *
  3231. * @param string $mode the blend mode to use
  3232. * @param float $opacity 0.0 fully transparent, 1.0 fully opaque
  3233. */
  3234. function setFillTransparency($mode, $opacity)
  3235. {
  3236. static $blend_modes = [
  3237. "Normal",
  3238. "Multiply",
  3239. "Screen",
  3240. "Overlay",
  3241. "Darken",
  3242. "Lighten",
  3243. "ColorDogde",
  3244. "ColorBurn",
  3245. "HardLight",
  3246. "SoftLight",
  3247. "Difference",
  3248. "Exclusion"
  3249. ];
  3250. if (!in_array($mode, $blend_modes)) {
  3251. $mode = "Normal";
  3252. }
  3253. if ($mode === $this->currentFillTransparency["mode"] &&
  3254. $opacity == $this->currentFillTransparency["opacity"]
  3255. ) {
  3256. return;
  3257. }
  3258. $this->currentFillTransparency["mode"] = $mode;
  3259. $this->currentFillTransparency["opacity"] = $opacity;
  3260. $options = [
  3261. "BM" => "/$mode",
  3262. "ca" => (float)$opacity,
  3263. ];
  3264. $this->setGraphicsState($options);
  3265. }
  3266. /**
  3267. * draw a line from one set of coordinates to another
  3268. *
  3269. * @param $x1
  3270. * @param $y1
  3271. * @param $x2
  3272. * @param $y2
  3273. * @param bool $stroke
  3274. */
  3275. function line($x1, $y1, $x2, $y2, $stroke = true)
  3276. {
  3277. $this->addContent(sprintf("\n%.3F %.3F m %.3F %.3F l", $x1, $y1, $x2, $y2));
  3278. if ($stroke) {
  3279. $this->addContent(' S');
  3280. }
  3281. }
  3282. /**
  3283. * draw a bezier curve based on 4 control points
  3284. *
  3285. * @param $x0
  3286. * @param $y0
  3287. * @param $x1
  3288. * @param $y1
  3289. * @param $x2
  3290. * @param $y2
  3291. * @param $x3
  3292. * @param $y3
  3293. */
  3294. function curve($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
  3295. {
  3296. // in the current line style, draw a bezier curve from (x0,y0) to (x3,y3) using the other two points
  3297. // as the control points for the curve.
  3298. $this->addContent(
  3299. sprintf("\n%.3F %.3F m %.3F %.3F %.3F %.3F %.3F %.3F c S", $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
  3300. );
  3301. }
  3302. /**
  3303. * draw a part of an ellipse
  3304. *
  3305. * @param $x0
  3306. * @param $y0
  3307. * @param $astart
  3308. * @param $afinish
  3309. * @param $r1
  3310. * @param int $r2
  3311. * @param int $angle
  3312. * @param int $nSeg
  3313. */
  3314. function partEllipse($x0, $y0, $astart, $afinish, $r1, $r2 = 0, $angle = 0, $nSeg = 8)
  3315. {
  3316. $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, false);
  3317. }
  3318. /**
  3319. * draw a filled ellipse
  3320. *
  3321. * @param $x0
  3322. * @param $y0
  3323. * @param $r1
  3324. * @param int $r2
  3325. * @param int $angle
  3326. * @param int $nSeg
  3327. * @param int $astart
  3328. * @param int $afinish
  3329. */
  3330. function filledEllipse($x0, $y0, $r1, $r2 = 0, $angle = 0, $nSeg = 8, $astart = 0, $afinish = 360)
  3331. {
  3332. $this->ellipse($x0, $y0, $r1, $r2, $angle, $nSeg, $astart, $afinish, true, true);
  3333. }
  3334. /**
  3335. * @param $x
  3336. * @param $y
  3337. */
  3338. function lineTo($x, $y)
  3339. {
  3340. $this->addContent(sprintf("\n%.3F %.3F l", $x, $y));
  3341. }
  3342. /**
  3343. * @param $x
  3344. * @param $y
  3345. */
  3346. function moveTo($x, $y)
  3347. {
  3348. $this->addContent(sprintf("\n%.3F %.3F m", $x, $y));
  3349. }
  3350. /**
  3351. * draw a bezier curve based on 4 control points
  3352. *
  3353. * @param $x1
  3354. * @param $y1
  3355. * @param $x2
  3356. * @param $y2
  3357. * @param $x3
  3358. * @param $y3
  3359. */
  3360. function curveTo($x1, $y1, $x2, $y2, $x3, $y3)
  3361. {
  3362. $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F %.3F %.3F c", $x1, $y1, $x2, $y2, $x3, $y3));
  3363. }
  3364. /**
  3365. * draw a bezier curve based on 4 control points
  3366. */
  3367. function quadTo($cpx, $cpy, $x, $y)
  3368. {
  3369. $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F v", $cpx, $cpy, $x, $y));
  3370. }
  3371. function closePath()
  3372. {
  3373. $this->addContent(' h');
  3374. }
  3375. function endPath()
  3376. {
  3377. $this->addContent(' n');
  3378. }
  3379. /**
  3380. * draw an ellipse
  3381. * note that the part and filled ellipse are just special cases of this function
  3382. *
  3383. * draws an ellipse in the current line style
  3384. * centered at $x0,$y0, radii $r1,$r2
  3385. * if $r2 is not set, then a circle is drawn
  3386. * from $astart to $afinish, measured in degrees, running anti-clockwise from the right hand side of the ellipse.
  3387. * nSeg is not allowed to be less than 2, as this will simply draw a line (and will even draw a
  3388. * pretty crappy shape at 2, as we are approximating with bezier curves.
  3389. *
  3390. * @param $x0
  3391. * @param $y0
  3392. * @param $r1
  3393. * @param int $r2
  3394. * @param int $angle
  3395. * @param int $nSeg
  3396. * @param int $astart
  3397. * @param int $afinish
  3398. * @param bool $close
  3399. * @param bool $fill
  3400. * @param bool $stroke
  3401. * @param bool $incomplete
  3402. */
  3403. function ellipse(
  3404. $x0,
  3405. $y0,
  3406. $r1,
  3407. $r2 = 0,
  3408. $angle = 0,
  3409. $nSeg = 8,
  3410. $astart = 0,
  3411. $afinish = 360,
  3412. $close = true,
  3413. $fill = false,
  3414. $stroke = true,
  3415. $incomplete = false
  3416. ) {
  3417. if ($r1 == 0) {
  3418. return;
  3419. }
  3420. if ($r2 == 0) {
  3421. $r2 = $r1;
  3422. }
  3423. if ($nSeg < 2) {
  3424. $nSeg = 2;
  3425. }
  3426. $astart = deg2rad((float)$astart);
  3427. $afinish = deg2rad((float)$afinish);
  3428. $totalAngle = $afinish - $astart;
  3429. $dt = $totalAngle / $nSeg;
  3430. $dtm = $dt / 3;
  3431. if ($angle != 0) {
  3432. $a = -1 * deg2rad((float)$angle);
  3433. $this->addContent(
  3434. sprintf("\n q %.3F %.3F %.3F %.3F %.3F %.3F cm", cos($a), -sin($a), sin($a), cos($a), $x0, $y0)
  3435. );
  3436. $x0 = 0;
  3437. $y0 = 0;
  3438. }
  3439. $t1 = $astart;
  3440. $a0 = $x0 + $r1 * cos($t1);
  3441. $b0 = $y0 + $r2 * sin($t1);
  3442. $c0 = -$r1 * sin($t1);
  3443. $d0 = $r2 * cos($t1);
  3444. if (!$incomplete) {
  3445. $this->addContent(sprintf("\n%.3F %.3F m ", $a0, $b0));
  3446. }
  3447. for ($i = 1; $i <= $nSeg; $i++) {
  3448. // draw this bit of the total curve
  3449. $t1 = $i * $dt + $astart;
  3450. $a1 = $x0 + $r1 * cos($t1);
  3451. $b1 = $y0 + $r2 * sin($t1);
  3452. $c1 = -$r1 * sin($t1);
  3453. $d1 = $r2 * cos($t1);
  3454. $this->addContent(
  3455. sprintf(
  3456. "\n%.3F %.3F %.3F %.3F %.3F %.3F c",
  3457. ($a0 + $c0 * $dtm),
  3458. ($b0 + $d0 * $dtm),
  3459. ($a1 - $c1 * $dtm),
  3460. ($b1 - $d1 * $dtm),
  3461. $a1,
  3462. $b1
  3463. )
  3464. );
  3465. $a0 = $a1;
  3466. $b0 = $b1;
  3467. $c0 = $c1;
  3468. $d0 = $d1;
  3469. }
  3470. if (!$incomplete) {
  3471. if ($fill) {
  3472. $this->addContent(' f');
  3473. }
  3474. if ($stroke) {
  3475. if ($close) {
  3476. $this->addContent(' s'); // small 's' signifies closing the path as well
  3477. } else {
  3478. $this->addContent(' S');
  3479. }
  3480. }
  3481. }
  3482. if ($angle != 0) {
  3483. $this->addContent(' Q');
  3484. }
  3485. }
  3486. /**
  3487. * this sets the line drawing style.
  3488. * width, is the thickness of the line in user units
  3489. * cap is the type of cap to put on the line, values can be 'butt','round','square'
  3490. * where the diffference between 'square' and 'butt' is that 'square' projects a flat end past the
  3491. * end of the line.
  3492. * join can be 'miter', 'round', 'bevel'
  3493. * dash is an array which sets the dash pattern, is a series of length values, which are the lengths of the
  3494. * on and off dashes.
  3495. * (2) represents 2 on, 2 off, 2 on , 2 off ...
  3496. * (2,1) is 2 on, 1 off, 2 on, 1 off.. etc
  3497. * phase is a modifier on the dash pattern which is used to shift the point at which the pattern starts.
  3498. *
  3499. * @param int $width
  3500. * @param string $cap
  3501. * @param string $join
  3502. * @param string $dash
  3503. * @param int $phase
  3504. */
  3505. function setLineStyle($width = 1, $cap = '', $join = '', $dash = '', $phase = 0)
  3506. {
  3507. // this is quite inefficient in that it sets all the parameters whenever 1 is changed, but will fix another day
  3508. $string = '';
  3509. if ($width > 0) {
  3510. $string .= "$width w";
  3511. }
  3512. $ca = ['butt' => 0, 'round' => 1, 'square' => 2];
  3513. if (isset($ca[$cap])) {
  3514. $string .= " $ca[$cap] J";
  3515. }
  3516. $ja = ['miter' => 0, 'round' => 1, 'bevel' => 2];
  3517. if (isset($ja[$join])) {
  3518. $string .= " $ja[$join] j";
  3519. }
  3520. if (is_array($dash)) {
  3521. $string .= ' [ ' . implode(' ', $dash) . " ] $phase d";
  3522. }
  3523. $this->currentLineStyle = $string;
  3524. $this->addContent("\n$string");
  3525. }
  3526. /**
  3527. * draw a polygon, the syntax for this is similar to the GD polygon command
  3528. *
  3529. * @param $p
  3530. * @param $np
  3531. * @param bool $f
  3532. */
  3533. function polygon($p, $np, $f = false)
  3534. {
  3535. $this->addContent(sprintf("\n%.3F %.3F m ", $p[0], $p[1]));
  3536. for ($i = 2; $i < $np * 2; $i = $i + 2) {
  3537. $this->addContent(sprintf("%.3F %.3F l ", $p[$i], $p[$i + 1]));
  3538. }
  3539. if ($f) {
  3540. $this->addContent(' f');
  3541. } else {
  3542. $this->addContent(' S');
  3543. }
  3544. }
  3545. /**
  3546. * a filled rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not
  3547. * the coordinates of the upper-right corner
  3548. *
  3549. * @param $x1
  3550. * @param $y1
  3551. * @param $width
  3552. * @param $height
  3553. */
  3554. function filledRectangle($x1, $y1, $width, $height)
  3555. {
  3556. $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re f", $x1, $y1, $width, $height));
  3557. }
  3558. /**
  3559. * draw a rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not
  3560. * the coordinates of the upper-right corner
  3561. *
  3562. * @param $x1
  3563. * @param $y1
  3564. * @param $width
  3565. * @param $height
  3566. */
  3567. function rectangle($x1, $y1, $width, $height)
  3568. {
  3569. $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re S", $x1, $y1, $width, $height));
  3570. }
  3571. /**
  3572. * draw a rectangle, note that it is the width and height of the rectangle which are the secondary parameters, not
  3573. * the coordinates of the upper-right corner
  3574. *
  3575. * @param $x1
  3576. * @param $y1
  3577. * @param $width
  3578. * @param $height
  3579. */
  3580. function rect($x1, $y1, $width, $height)
  3581. {
  3582. $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re", $x1, $y1, $width, $height));
  3583. }
  3584. function stroke()
  3585. {
  3586. $this->addContent("\nS");
  3587. }
  3588. function fill()
  3589. {
  3590. $this->addContent("\nf" . ($this->fillRule === "evenodd" ? "*" : ""));
  3591. }
  3592. function fillStroke()
  3593. {
  3594. $this->addContent("\nb" . ($this->fillRule === "evenodd" ? "*" : ""));
  3595. }
  3596. /**
  3597. * @param string $subtype
  3598. * @param integer $x
  3599. * @param integer $y
  3600. * @param integer $w
  3601. * @param integer $h
  3602. * @return int
  3603. */
  3604. function addXObject($subtype, $x, $y, $w, $h)
  3605. {
  3606. $id = ++$this->numObj;
  3607. $this->o_xobject($id, 'new', ['Subtype' => $subtype, 'bbox' => [$x, $y, $w, $h]]);
  3608. return $id;
  3609. }
  3610. /**
  3611. * @param integer $numXObject
  3612. * @param string $type
  3613. * @param array $options
  3614. */
  3615. function setXObjectResource($numXObject, $type, $options)
  3616. {
  3617. if (in_array($type, ['procset', 'font', 'xObject'])) {
  3618. $this->o_xobject($numXObject, $type, $options);
  3619. }
  3620. }
  3621. /**
  3622. * add signature
  3623. *
  3624. * $fieldSigId = $cpdf->addFormField(Cpdf::ACROFORM_FIELD_SIG, 'Signature1', 0, 0, 0, 0, 0);
  3625. *
  3626. * $signatureId = $cpdf->addSignature([
  3627. * 'signcert' => file_get_contents('dompdf.crt'),
  3628. * 'privkey' => file_get_contents('dompdf.key'),
  3629. * 'password' => 'password',
  3630. * 'name' => 'DomPDF DEMO',
  3631. * 'location' => 'Home',
  3632. * 'reason' => 'First Form',
  3633. * 'contactinfo' => 'info'
  3634. * ]);
  3635. * $cpdf->setFormFieldValue($fieldSigId, "$signatureId 0 R");
  3636. *
  3637. * @param string $signcert
  3638. * @param string $privkey
  3639. * @param string $password
  3640. * @param string|null $name
  3641. * @param string|null $location
  3642. * @param string|null $reason
  3643. * @param string|null $contactinfo
  3644. * @return int
  3645. */
  3646. function addSignature($signcert, $privkey, $password = '', $name = null, $location = null, $reason = null, $contactinfo = null) {
  3647. $sigId = ++$this->numObj;
  3648. $this->o_sig($sigId, 'new', [
  3649. 'SignCert' => $signcert,
  3650. 'PrivKey' => $privkey,
  3651. 'Password' => $password,
  3652. 'Name' => $name,
  3653. 'Location' => $location,
  3654. 'Reason' => $reason,
  3655. 'ContactInfo' => $contactinfo
  3656. ]);
  3657. return $sigId;
  3658. }
  3659. /**
  3660. * add field to form
  3661. *
  3662. * @param string $type ACROFORM_FIELD_*
  3663. * @param string $name
  3664. * @param $x0
  3665. * @param $y0
  3666. * @param $x1
  3667. * @param $y1
  3668. * @param integer $ff Field Flag ACROFORM_FIELD_*_*
  3669. * @param float $size
  3670. * @param array $color
  3671. * @return int
  3672. */
  3673. public function addFormField($type, $name, $x0, $y0, $x1, $y1, $ff = 0, $size = 10.0, $color = [0, 0, 0])
  3674. {
  3675. if (!$this->numFonts) {
  3676. $this->selectFont($this->defaultFont);
  3677. }
  3678. $color = implode(' ', $color) . ' rg';
  3679. $currentFontNum = $this->currentFontNum;
  3680. $font = array_filter($this->objects[$this->currentNode]['info']['fonts'],
  3681. function($item) use ($currentFontNum) { return $item['fontNum'] == $currentFontNum; });
  3682. $this->o_acroform($this->acroFormId, 'font',
  3683. ['objNum' => $font[0]['objNum'], 'fontNum' => $font[0]['fontNum']]);
  3684. $fieldId = ++$this->numObj;
  3685. $this->o_field($fieldId, 'new', [
  3686. 'rect' => [$x0, $y0, $x1, $y1],
  3687. 'F' => 4,
  3688. 'FT' => "/$type",
  3689. 'T' => $name,
  3690. 'Ff' => $ff,
  3691. 'pageid' => $this->currentPage,
  3692. 'da' => "$color /F$this->currentFontNum " . sprintf('%.1F Tf ', $size)
  3693. ]);
  3694. return $fieldId;
  3695. }
  3696. /**
  3697. * set Field value
  3698. *
  3699. * @param integer $numFieldObj
  3700. * @param string $value
  3701. */
  3702. public function setFormFieldValue($numFieldObj, $value)
  3703. {
  3704. $this->o_field($numFieldObj, 'set', ['value' => $value]);
  3705. }
  3706. /**
  3707. * set Field value (reference)
  3708. *
  3709. * @param integer $numFieldObj
  3710. * @param integer $numObj Object number
  3711. */
  3712. public function setFormFieldRefValue($numFieldObj, $numObj)
  3713. {
  3714. $this->o_field($numFieldObj, 'set', ['refvalue' => $numObj]);
  3715. }
  3716. /**
  3717. * set Field Appearanc (reference)
  3718. *
  3719. * @param integer $numFieldObj
  3720. * @param integer $normalNumObj
  3721. * @param integer|null $rolloverNumObj
  3722. * @param integer|null $downNumObj
  3723. */
  3724. public function setFormFieldAppearance($numFieldObj, $normalNumObj, $rolloverNumObj = null, $downNumObj = null)
  3725. {
  3726. $appearance['N'] = $normalNumObj;
  3727. if ($rolloverNumObj !== null) {
  3728. $appearance['R'] = $rolloverNumObj;
  3729. }
  3730. if ($downNumObj !== null) {
  3731. $appearance['D'] = $downNumObj;
  3732. }
  3733. $this->o_field($numFieldObj, 'set', ['appearance' => $appearance]);
  3734. }
  3735. /**
  3736. * set Choice Field option values
  3737. *
  3738. * @param integer $numFieldObj
  3739. * @param array $value
  3740. */
  3741. public function setFormFieldOpt($numFieldObj, $value)
  3742. {
  3743. $this->o_field($numFieldObj, 'set', ['options' => $value]);
  3744. }
  3745. /**
  3746. * add form to document
  3747. *
  3748. * @param integer $sigFlags
  3749. * @param boolean $needAppearances
  3750. */
  3751. public function addForm($sigFlags = 0, $needAppearances = false)
  3752. {
  3753. $this->acroFormId = ++$this->numObj;
  3754. $this->o_acroform($this->acroFormId, 'new', [
  3755. 'NeedAppearances' => $needAppearances ? 'true' : 'false',
  3756. 'SigFlags' => $sigFlags
  3757. ]);
  3758. }
  3759. /**
  3760. * save the current graphic state
  3761. */
  3762. function save()
  3763. {
  3764. // we must reset the color cache or it will keep bad colors after clipping
  3765. $this->currentColor = null;
  3766. $this->currentStrokeColor = null;
  3767. $this->addContent("\nq");
  3768. }
  3769. /**
  3770. * restore the last graphic state
  3771. */
  3772. function restore()
  3773. {
  3774. // we must reset the color cache or it will keep bad colors after clipping
  3775. $this->currentColor = null;
  3776. $this->currentStrokeColor = null;
  3777. $this->addContent("\nQ");
  3778. }
  3779. /**
  3780. * draw a clipping rectangle, all the elements added after this will be clipped
  3781. *
  3782. * @param $x1
  3783. * @param $y1
  3784. * @param $width
  3785. * @param $height
  3786. */
  3787. function clippingRectangle($x1, $y1, $width, $height)
  3788. {
  3789. $this->save();
  3790. $this->addContent(sprintf("\n%.3F %.3F %.3F %.3F re W n", $x1, $y1, $width, $height));
  3791. }
  3792. /**
  3793. * draw a clipping rounded rectangle, all the elements added after this will be clipped
  3794. *
  3795. * @param $x1
  3796. * @param $y1
  3797. * @param $w
  3798. * @param $h
  3799. * @param $rTL
  3800. * @param $rTR
  3801. * @param $rBR
  3802. * @param $rBL
  3803. */
  3804. function clippingRectangleRounded($x1, $y1, $w, $h, $rTL, $rTR, $rBR, $rBL)
  3805. {
  3806. $this->save();
  3807. // start: top edge, left end
  3808. $this->addContent(sprintf("\n%.3F %.3F m ", $x1, $y1 - $rTL + $h));
  3809. // line: bottom edge, left end
  3810. $this->addContent(sprintf("\n%.3F %.3F l ", $x1, $y1 + $rBL));
  3811. // curve: bottom-left corner
  3812. $this->ellipse($x1 + $rBL, $y1 + $rBL, $rBL, 0, 0, 8, 180, 270, false, false, false, true);
  3813. // line: right edge, bottom end
  3814. $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w - $rBR, $y1));
  3815. // curve: bottom-right corner
  3816. $this->ellipse($x1 + $w - $rBR, $y1 + $rBR, $rBR, 0, 0, 8, 270, 360, false, false, false, true);
  3817. // line: right edge, top end
  3818. $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $w, $y1 + $h - $rTR));
  3819. // curve: bottom-right corner
  3820. $this->ellipse($x1 + $w - $rTR, $y1 + $h - $rTR, $rTR, 0, 0, 8, 0, 90, false, false, false, true);
  3821. // line: bottom edge, right end
  3822. $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rTL, $y1 + $h));
  3823. // curve: top-right corner
  3824. $this->ellipse($x1 + $rTL, $y1 + $h - $rTL, $rTL, 0, 0, 8, 90, 180, false, false, false, true);
  3825. // line: top edge, left end
  3826. $this->addContent(sprintf("\n%.3F %.3F l ", $x1 + $rBL, $y1));
  3827. // Close & clip
  3828. $this->addContent(" W n");
  3829. }
  3830. /**
  3831. * ends the last clipping shape
  3832. */
  3833. function clippingEnd()
  3834. {
  3835. $this->restore();
  3836. }
  3837. /**
  3838. * scale
  3839. *
  3840. * @param float $s_x scaling factor for width as percent
  3841. * @param float $s_y scaling factor for height as percent
  3842. * @param float $x Origin abscissa
  3843. * @param float $y Origin ordinate
  3844. */
  3845. function scale($s_x, $s_y, $x, $y)
  3846. {
  3847. $y = $this->currentPageSize["height"] - $y;
  3848. $tm = [
  3849. $s_x,
  3850. 0,
  3851. 0,
  3852. $s_y,
  3853. $x * (1 - $s_x),
  3854. $y * (1 - $s_y)
  3855. ];
  3856. $this->transform($tm);
  3857. }
  3858. /**
  3859. * translate
  3860. *
  3861. * @param float $t_x movement to the right
  3862. * @param float $t_y movement to the bottom
  3863. */
  3864. function translate($t_x, $t_y)
  3865. {
  3866. $tm = [
  3867. 1,
  3868. 0,
  3869. 0,
  3870. 1,
  3871. $t_x,
  3872. -$t_y
  3873. ];
  3874. $this->transform($tm);
  3875. }
  3876. /**
  3877. * rotate
  3878. *
  3879. * @param float $angle angle in degrees for counter-clockwise rotation
  3880. * @param float $x Origin abscissa
  3881. * @param float $y Origin ordinate
  3882. */
  3883. function rotate($angle, $x, $y)
  3884. {
  3885. $y = $this->currentPageSize["height"] - $y;
  3886. $a = deg2rad($angle);
  3887. $cos_a = cos($a);
  3888. $sin_a = sin($a);
  3889. $tm = [
  3890. $cos_a,
  3891. -$sin_a,
  3892. $sin_a,
  3893. $cos_a,
  3894. $x - $sin_a * $y - $cos_a * $x,
  3895. $y - $cos_a * $y + $sin_a * $x,
  3896. ];
  3897. $this->transform($tm);
  3898. }
  3899. /**
  3900. * skew
  3901. *
  3902. * @param float $angle_x
  3903. * @param float $angle_y
  3904. * @param float $x Origin abscissa
  3905. * @param float $y Origin ordinate
  3906. */
  3907. function skew($angle_x, $angle_y, $x, $y)
  3908. {
  3909. $y = $this->currentPageSize["height"] - $y;
  3910. $tan_x = tan(deg2rad($angle_x));
  3911. $tan_y = tan(deg2rad($angle_y));
  3912. $tm = [
  3913. 1,
  3914. -$tan_y,
  3915. -$tan_x,
  3916. 1,
  3917. $tan_x * $y,
  3918. $tan_y * $x,
  3919. ];
  3920. $this->transform($tm);
  3921. }
  3922. /**
  3923. * apply graphic transformations
  3924. *
  3925. * @param array $tm transformation matrix
  3926. */
  3927. function transform($tm)
  3928. {
  3929. $this->addContent(vsprintf("\n %.3F %.3F %.3F %.3F %.3F %.3F cm", $tm));
  3930. }
  3931. /**
  3932. * add a new page to the document
  3933. * this also makes the new page the current active object
  3934. *
  3935. * @param int $insert
  3936. * @param int $id
  3937. * @param string $pos
  3938. * @return int
  3939. */
  3940. function newPage($insert = 0, $id = 0, $pos = 'after')
  3941. {
  3942. // if there is a state saved, then go up the stack closing them
  3943. // then on the new page, re-open them with the right setings
  3944. if ($this->nStateStack) {
  3945. for ($i = $this->nStateStack; $i >= 1; $i--) {
  3946. $this->restoreState($i);
  3947. }
  3948. }
  3949. $this->numObj++;
  3950. if ($insert) {
  3951. // the id from the ezPdf class is the id of the contents of the page, not the page object itself
  3952. // query that object to find the parent
  3953. $rid = $this->objects[$id]['onPage'];
  3954. $opt = ['rid' => $rid, 'pos' => $pos];
  3955. $this->o_page($this->numObj, 'new', $opt);
  3956. } else {
  3957. $this->o_page($this->numObj, 'new');
  3958. }
  3959. // if there is a stack saved, then put that onto the page
  3960. if ($this->nStateStack) {
  3961. for ($i = 1; $i <= $this->nStateStack; $i++) {
  3962. $this->saveState($i);
  3963. }
  3964. }
  3965. // and if there has been a stroke or fill color set, then transfer them
  3966. if (isset($this->currentColor)) {
  3967. $this->setColor($this->currentColor, true);
  3968. }
  3969. if (isset($this->currentStrokeColor)) {
  3970. $this->setStrokeColor($this->currentStrokeColor, true);
  3971. }
  3972. // if there is a line style set, then put this in too
  3973. if (mb_strlen($this->currentLineStyle, '8bit')) {
  3974. $this->addContent("\n$this->currentLineStyle");
  3975. }
  3976. // the call to the o_page object set currentContents to the present page, so this can be returned as the page id
  3977. return $this->currentContents;
  3978. }
  3979. /**
  3980. * Streams the PDF to the client.
  3981. *
  3982. * @param string $filename The filename to present to the client.
  3983. * @param array $options Associative array: 'compress' => 1 or 0 (default 1); 'Attachment' => 1 or 0 (default 1).
  3984. */
  3985. function stream($filename = "document.pdf", $options = [])
  3986. {
  3987. if (headers_sent()) {
  3988. die("Unable to stream pdf: headers already sent");
  3989. }
  3990. if (!isset($options["compress"])) $options["compress"] = true;
  3991. if (!isset($options["Attachment"])) $options["Attachment"] = true;
  3992. $debug = !$options['compress'];
  3993. $tmp = ltrim($this->output($debug));
  3994. header("Cache-Control: private");
  3995. header("Content-Type: application/pdf");
  3996. header("Content-Length: " . mb_strlen($tmp, "8bit"));
  3997. $filename = str_replace(["\n", "'"], "", basename($filename, ".pdf")) . ".pdf";
  3998. $attachment = $options["Attachment"] ? "attachment" : "inline";
  3999. $encoding = mb_detect_encoding($filename);
  4000. $fallbackfilename = mb_convert_encoding($filename, "ISO-8859-1", $encoding);
  4001. $fallbackfilename = str_replace("\"", "", $fallbackfilename);
  4002. $encodedfilename = rawurlencode($filename);
  4003. $contentDisposition = "Content-Disposition: $attachment; filename=\"$fallbackfilename\"";
  4004. if ($fallbackfilename !== $filename) {
  4005. $contentDisposition .= "; filename*=UTF-8''$encodedfilename";
  4006. }
  4007. header($contentDisposition);
  4008. echo $tmp;
  4009. flush();
  4010. }
  4011. /**
  4012. * return the height in units of the current font in the given size
  4013. *
  4014. * @param $size
  4015. * @return float|int
  4016. */
  4017. function getFontHeight($size)
  4018. {
  4019. if (!$this->numFonts) {
  4020. $this->selectFont($this->defaultFont);
  4021. }
  4022. $font = $this->fonts[$this->currentFont];
  4023. // for the current font, and the given size, what is the height of the font in user units
  4024. if (isset($font['Ascender']) && isset($font['Descender'])) {
  4025. $h = $font['Ascender'] - $font['Descender'];
  4026. } else {
  4027. $h = $font['FontBBox'][3] - $font['FontBBox'][1];
  4028. }
  4029. // have to adjust by a font offset for Windows fonts. unfortunately it looks like
  4030. // the bounding box calculations are wrong and I don't know why.
  4031. if (isset($font['FontHeightOffset'])) {
  4032. // For CourierNew from Windows this needs to be -646 to match the
  4033. // Adobe native Courier font.
  4034. //
  4035. // For FreeMono from GNU this needs to be -337 to match the
  4036. // Courier font.
  4037. //
  4038. // Both have been added manually to the .afm and .ufm files.
  4039. $h += (int)$font['FontHeightOffset'];
  4040. }
  4041. return $size * $h / 1000;
  4042. }
  4043. /**
  4044. * @param $size
  4045. * @return float|int
  4046. */
  4047. function getFontXHeight($size)
  4048. {
  4049. if (!$this->numFonts) {
  4050. $this->selectFont($this->defaultFont);
  4051. }
  4052. $font = $this->fonts[$this->currentFont];
  4053. // for the current font, and the given size, what is the height of the font in user units
  4054. if (isset($font['XHeight'])) {
  4055. $xh = $font['Ascender'] - $font['Descender'];
  4056. } else {
  4057. $xh = $this->getFontHeight($size) / 2;
  4058. }
  4059. return $size * $xh / 1000;
  4060. }
  4061. /**
  4062. * return the font descender, this will normally return a negative number
  4063. * if you add this number to the baseline, you get the level of the bottom of the font
  4064. * it is in the pdf user units
  4065. *
  4066. * @param $size
  4067. * @return float|int
  4068. */
  4069. function getFontDescender($size)
  4070. {
  4071. // note that this will most likely return a negative value
  4072. if (!$this->numFonts) {
  4073. $this->selectFont($this->defaultFont);
  4074. }
  4075. //$h = $this->fonts[$this->currentFont]['FontBBox'][1];
  4076. $h = $this->fonts[$this->currentFont]['Descender'];
  4077. return $size * $h / 1000;
  4078. }
  4079. /**
  4080. * filter the text, this is applied to all text just before being inserted into the pdf document
  4081. * it escapes the various things that need to be escaped, and so on
  4082. *
  4083. * @access private
  4084. *
  4085. * @param $text
  4086. * @param bool $bom
  4087. * @param bool $convert_encoding
  4088. * @return string
  4089. */
  4090. function filterText($text, $bom = true, $convert_encoding = true)
  4091. {
  4092. if (!$this->numFonts) {
  4093. $this->selectFont($this->defaultFont);
  4094. }
  4095. if ($convert_encoding) {
  4096. $cf = $this->currentFont;
  4097. if (isset($this->fonts[$cf]) && $this->fonts[$cf]['isUnicode']) {
  4098. $text = $this->utf8toUtf16BE($text, $bom);
  4099. } else {
  4100. //$text = html_entity_decode($text, ENT_QUOTES);
  4101. $text = mb_convert_encoding($text, self::$targetEncoding, 'UTF-8');
  4102. }
  4103. } else if ($bom) {
  4104. $text = $this->utf8toUtf16BE($text, $bom);
  4105. }
  4106. // the chr(13) substitution fixes a bug seen in TCPDF (bug #1421290)
  4107. return strtr($text, [')' => '\\)', '(' => '\\(', '\\' => '\\\\', chr(13) => '\r']);
  4108. }
  4109. /**
  4110. * return array containing codepoints (UTF-8 character values) for the
  4111. * string passed in.
  4112. *
  4113. * based on the excellent TCPDF code by Nicola Asuni and the
  4114. * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html
  4115. *
  4116. * @access private
  4117. * @author Orion Richardson
  4118. * @since January 5, 2008
  4119. *
  4120. * @param string $text UTF-8 string to process
  4121. *
  4122. * @return array UTF-8 codepoints array for the string
  4123. */
  4124. function utf8toCodePointsArray(&$text)
  4125. {
  4126. $length = mb_strlen($text, '8bit'); // http://www.php.net/manual/en/function.mb-strlen.php#77040
  4127. $unicode = []; // array containing unicode values
  4128. $bytes = []; // array containing single character byte sequences
  4129. $numbytes = 1; // number of octets needed to represent the UTF-8 character
  4130. for ($i = 0; $i < $length; $i++) {
  4131. $c = ord($text[$i]); // get one string character at time
  4132. if (count($bytes) === 0) { // get starting octect
  4133. if ($c <= 0x7F) {
  4134. $unicode[] = $c; // use the character "as is" because is ASCII
  4135. $numbytes = 1;
  4136. } elseif (($c >> 0x05) === 0x06) { // 2 bytes character (0x06 = 110 BIN)
  4137. $bytes[] = ($c - 0xC0) << 0x06;
  4138. $numbytes = 2;
  4139. } elseif (($c >> 0x04) === 0x0E) { // 3 bytes character (0x0E = 1110 BIN)
  4140. $bytes[] = ($c - 0xE0) << 0x0C;
  4141. $numbytes = 3;
  4142. } elseif (($c >> 0x03) === 0x1E) { // 4 bytes character (0x1E = 11110 BIN)
  4143. $bytes[] = ($c - 0xF0) << 0x12;
  4144. $numbytes = 4;
  4145. } else {
  4146. // use replacement character for other invalid sequences
  4147. $unicode[] = 0xFFFD;
  4148. $bytes = [];
  4149. $numbytes = 1;
  4150. }
  4151. } elseif (($c >> 0x06) === 0x02) { // bytes 2, 3 and 4 must start with 0x02 = 10 BIN
  4152. $bytes[] = $c - 0x80;
  4153. if (count($bytes) === $numbytes) {
  4154. // compose UTF-8 bytes to a single unicode value
  4155. $c = $bytes[0];
  4156. for ($j = 1; $j < $numbytes; $j++) {
  4157. $c += ($bytes[$j] << (($numbytes - $j - 1) * 0x06));
  4158. }
  4159. if ((($c >= 0xD800) and ($c <= 0xDFFF)) or ($c >= 0x10FFFF)) {
  4160. // The definition of UTF-8 prohibits encoding character numbers between
  4161. // U+D800 and U+DFFF, which are reserved for use with the UTF-16
  4162. // encoding form (as surrogate pairs) and do not directly represent
  4163. // characters.
  4164. $unicode[] = 0xFFFD; // use replacement character
  4165. } else {
  4166. $unicode[] = $c; // add char to array
  4167. }
  4168. // reset data for next char
  4169. $bytes = [];
  4170. $numbytes = 1;
  4171. }
  4172. } else {
  4173. // use replacement character for other invalid sequences
  4174. $unicode[] = 0xFFFD;
  4175. $bytes = [];
  4176. $numbytes = 1;
  4177. }
  4178. }
  4179. return $unicode;
  4180. }
  4181. /**
  4182. * convert UTF-8 to UTF-16 with an additional byte order marker
  4183. * at the front if required.
  4184. *
  4185. * based on the excellent TCPDF code by Nicola Asuni and the
  4186. * RFC for UTF-8 at http://www.faqs.org/rfcs/rfc3629.html
  4187. *
  4188. * @access private
  4189. * @author Orion Richardson
  4190. * @since January 5, 2008
  4191. *
  4192. * @param string $text UTF-8 string to process
  4193. * @param boolean $bom whether to add the byte order marker
  4194. *
  4195. * @return string UTF-16 result string
  4196. */
  4197. function utf8toUtf16BE(&$text, $bom = true)
  4198. {
  4199. $out = $bom ? "\xFE\xFF" : '';
  4200. $unicode = $this->utf8toCodePointsArray($text);
  4201. foreach ($unicode as $c) {
  4202. if ($c === 0xFFFD) {
  4203. $out .= "\xFF\xFD"; // replacement character
  4204. } elseif ($c < 0x10000) {
  4205. $out .= chr($c >> 0x08) . chr($c & 0xFF);
  4206. } else {
  4207. $c -= 0x10000;
  4208. $w1 = 0xD800 | ($c >> 0x10);
  4209. $w2 = 0xDC00 | ($c & 0x3FF);
  4210. $out .= chr($w1 >> 0x08) . chr($w1 & 0xFF) . chr($w2 >> 0x08) . chr($w2 & 0xFF);
  4211. }
  4212. }
  4213. return $out;
  4214. }
  4215. /**
  4216. * given a start position and information about how text is to be laid out, calculate where
  4217. * on the page the text will end
  4218. *
  4219. * @param $x
  4220. * @param $y
  4221. * @param $angle
  4222. * @param $size
  4223. * @param $wa
  4224. * @param $text
  4225. * @return array
  4226. */
  4227. private function getTextPosition($x, $y, $angle, $size, $wa, $text)
  4228. {
  4229. // given this information return an array containing x and y for the end position as elements 0 and 1
  4230. $w = $this->getTextWidth($size, $text);
  4231. // need to adjust for the number of spaces in this text
  4232. $words = explode(' ', $text);
  4233. $nspaces = count($words) - 1;
  4234. $w += $wa * $nspaces;
  4235. $a = deg2rad((float)$angle);
  4236. return [cos($a) * $w + $x, -sin($a) * $w + $y];
  4237. }
  4238. /**
  4239. * Callback method used by smallCaps
  4240. *
  4241. * @param array $matches
  4242. *
  4243. * @return string
  4244. */
  4245. function toUpper($matches)
  4246. {
  4247. return mb_strtoupper($matches[0]);
  4248. }
  4249. function concatMatches($matches)
  4250. {
  4251. $str = "";
  4252. foreach ($matches as $match) {
  4253. $str .= $match[0];
  4254. }
  4255. return $str;
  4256. }
  4257. /**
  4258. * register text for font subsetting
  4259. *
  4260. * @param $font
  4261. * @param $text
  4262. */
  4263. function registerText($font, $text)
  4264. {
  4265. if (!$this->isUnicode || in_array(mb_strtolower(basename($font)), self::$coreFonts)) {
  4266. return;
  4267. }
  4268. if (!isset($this->stringSubsets[$font])) {
  4269. $this->stringSubsets[$font] = [];
  4270. }
  4271. $this->stringSubsets[$font] = array_unique(
  4272. array_merge($this->stringSubsets[$font], $this->utf8toCodePointsArray($text))
  4273. );
  4274. }
  4275. /**
  4276. * add text to the document, at a specified location, size and angle on the page
  4277. *
  4278. * @param $x
  4279. * @param $y
  4280. * @param $size
  4281. * @param $text
  4282. * @param int $angle
  4283. * @param int $wordSpaceAdjust
  4284. * @param int $charSpaceAdjust
  4285. * @param bool $smallCaps
  4286. */
  4287. function addText($x, $y, $size, $text, $angle = 0, $wordSpaceAdjust = 0, $charSpaceAdjust = 0, $smallCaps = false)
  4288. {
  4289. if (!$this->numFonts) {
  4290. $this->selectFont($this->defaultFont);
  4291. }
  4292. $text = str_replace(["\r", "\n"], "", $text);
  4293. if ($smallCaps) {
  4294. preg_match_all("/(\P{Ll}+)/u", $text, $matches, PREG_SET_ORDER);
  4295. $lower = $this->concatMatches($matches);
  4296. d($lower);
  4297. preg_match_all("/(\p{Ll}+)/u", $text, $matches, PREG_SET_ORDER);
  4298. $other = $this->concatMatches($matches);
  4299. d($other);
  4300. //$text = preg_replace_callback("/\p{Ll}/u", array($this, "toUpper"), $text);
  4301. }
  4302. // if there are any open callbacks, then they should be called, to show the start of the line
  4303. if ($this->nCallback > 0) {
  4304. for ($i = $this->nCallback; $i > 0; $i--) {
  4305. // call each function
  4306. $info = [
  4307. 'x' => $x,
  4308. 'y' => $y,
  4309. 'angle' => $angle,
  4310. 'status' => 'sol',
  4311. 'p' => $this->callback[$i]['p'],
  4312. 'nCallback' => $this->callback[$i]['nCallback'],
  4313. 'height' => $this->callback[$i]['height'],
  4314. 'descender' => $this->callback[$i]['descender']
  4315. ];
  4316. $func = $this->callback[$i]['f'];
  4317. $this->$func($info);
  4318. }
  4319. }
  4320. if ($angle == 0) {
  4321. $this->addContent(sprintf("\nBT %.3F %.3F Td", $x, $y));
  4322. } else {
  4323. $a = deg2rad((float)$angle);
  4324. $this->addContent(
  4325. sprintf("\nBT %.3F %.3F %.3F %.3F %.3F %.3F Tm", cos($a), -sin($a), sin($a), cos($a), $x, $y)
  4326. );
  4327. }
  4328. if ($wordSpaceAdjust != 0) {
  4329. $this->addContent(sprintf(" %.3F Tw", $wordSpaceAdjust));
  4330. }
  4331. if ($charSpaceAdjust != 0) {
  4332. $this->addContent(sprintf(" %.3F Tc", $charSpaceAdjust));
  4333. }
  4334. $len = mb_strlen($text);
  4335. $start = 0;
  4336. if ($start < $len) {
  4337. $part = $text; // OAR - Don't need this anymore, given that $start always equals zero. substr($text, $start);
  4338. $place_text = $this->filterText($part, false);
  4339. // modify unicode text so that extra word spacing is manually implemented (bug #)
  4340. if ($this->fonts[$this->currentFont]['isUnicode'] && $wordSpaceAdjust != 0) {
  4341. $space_scale = 1000 / $size;
  4342. $place_text = str_replace("\x00\x20", "\x00\x20)\x00\x20" . (-round($space_scale * $wordSpaceAdjust)) . "\x00\x20(", $place_text);
  4343. }
  4344. $this->addContent(" /F$this->currentFontNum " . sprintf('%.1F Tf ', $size));
  4345. $this->addContent(" [($place_text)] TJ");
  4346. }
  4347. if ($wordSpaceAdjust != 0) {
  4348. $this->addContent(sprintf(" %.3F Tw", 0));
  4349. }
  4350. if ($charSpaceAdjust != 0) {
  4351. $this->addContent(sprintf(" %.3F Tc", 0));
  4352. }
  4353. $this->addContent(' ET');
  4354. // if there are any open callbacks, then they should be called, to show the end of the line
  4355. if ($this->nCallback > 0) {
  4356. for ($i = $this->nCallback; $i > 0; $i--) {
  4357. // call each function
  4358. $tmp = $this->getTextPosition($x, $y, $angle, $size, $wordSpaceAdjust, $text);
  4359. $info = [
  4360. 'x' => $tmp[0],
  4361. 'y' => $tmp[1],
  4362. 'angle' => $angle,
  4363. 'status' => 'eol',
  4364. 'p' => $this->callback[$i]['p'],
  4365. 'nCallback' => $this->callback[$i]['nCallback'],
  4366. 'height' => $this->callback[$i]['height'],
  4367. 'descender' => $this->callback[$i]['descender']
  4368. ];
  4369. $func = $this->callback[$i]['f'];
  4370. $this->$func($info);
  4371. }
  4372. }
  4373. if ($this->fonts[$this->currentFont]['isSubsetting']) {
  4374. $this->registerText($this->currentFont, $text);
  4375. }
  4376. }
  4377. /**
  4378. * calculate how wide a given text string will be on a page, at a given size.
  4379. * this can be called externally, but is also used by the other class functions
  4380. *
  4381. * @param $size
  4382. * @param $text
  4383. * @param int $word_spacing
  4384. * @param int $char_spacing
  4385. * @return float|int
  4386. */
  4387. function getTextWidth($size, $text, $word_spacing = 0, $char_spacing = 0)
  4388. {
  4389. static $ord_cache = [];
  4390. // this function should not change any of the settings, though it will need to
  4391. // track any directives which change during calculation, so copy them at the start
  4392. // and put them back at the end.
  4393. $store_currentTextState = $this->currentTextState;
  4394. if (!$this->numFonts) {
  4395. $this->selectFont($this->defaultFont);
  4396. }
  4397. $text = str_replace(["\r", "\n"], "", $text);
  4398. // converts a number or a float to a string so it can get the width
  4399. $text = "$text";
  4400. // hmm, this is where it all starts to get tricky - use the font information to
  4401. // calculate the width of each character, add them up and convert to user units
  4402. $w = 0;
  4403. $cf = $this->currentFont;
  4404. $current_font = $this->fonts[$cf];
  4405. $space_scale = 1000 / ($size > 0 ? $size : 1);
  4406. $n_spaces = 0;
  4407. if ($current_font['isUnicode']) {
  4408. // for Unicode, use the code points array to calculate width rather
  4409. // than just the string itself
  4410. $unicode = $this->utf8toCodePointsArray($text);
  4411. foreach ($unicode as $char) {
  4412. // check if we have to replace character
  4413. if (isset($current_font['differences'][$char])) {
  4414. $char = $current_font['differences'][$char];
  4415. }
  4416. if (isset($current_font['C'][$char])) {
  4417. $char_width = $current_font['C'][$char];
  4418. // add the character width
  4419. $w += $char_width;
  4420. // add additional padding for space
  4421. if (isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space') { // Space
  4422. $w += $word_spacing * $space_scale;
  4423. $n_spaces++;
  4424. }
  4425. }
  4426. }
  4427. // add additional char spacing
  4428. if ($char_spacing != 0) {
  4429. $w += $char_spacing * $space_scale * (count($unicode) + $n_spaces);
  4430. }
  4431. } else {
  4432. // If CPDF is in Unicode mode but the current font does not support Unicode we need to convert the character set to Windows-1252
  4433. if ($this->isUnicode) {
  4434. $text = mb_convert_encoding($text, 'Windows-1252', 'UTF-8');
  4435. }
  4436. $len = mb_strlen($text, 'Windows-1252');
  4437. for ($i = 0; $i < $len; $i++) {
  4438. $c = $text[$i];
  4439. $char = isset($ord_cache[$c]) ? $ord_cache[$c] : ($ord_cache[$c] = ord($c));
  4440. // check if we have to replace character
  4441. if (isset($current_font['differences'][$char])) {
  4442. $char = $current_font['differences'][$char];
  4443. }
  4444. if (isset($current_font['C'][$char])) {
  4445. $char_width = $current_font['C'][$char];
  4446. // add the character width
  4447. $w += $char_width;
  4448. // add additional padding for space
  4449. if (isset($current_font['codeToName'][$char]) && $current_font['codeToName'][$char] === 'space') { // Space
  4450. $w += $word_spacing * $space_scale;
  4451. $n_spaces++;
  4452. }
  4453. }
  4454. }
  4455. // add additional char spacing
  4456. if ($char_spacing != 0) {
  4457. $w += $char_spacing * $space_scale * ($len + $n_spaces);
  4458. }
  4459. }
  4460. $this->currentTextState = $store_currentTextState;
  4461. $this->setCurrentFont();
  4462. return $w * $size / 1000;
  4463. }
  4464. /**
  4465. * this will be called at a new page to return the state to what it was on the
  4466. * end of the previous page, before the stack was closed down
  4467. * This is to get around not being able to have open 'q' across pages
  4468. *
  4469. * @param int $pageEnd
  4470. */
  4471. function saveState($pageEnd = 0)
  4472. {
  4473. if ($pageEnd) {
  4474. // this will be called at a new page to return the state to what it was on the
  4475. // end of the previous page, before the stack was closed down
  4476. // This is to get around not being able to have open 'q' across pages
  4477. $opt = $this->stateStack[$pageEnd];
  4478. // ok to use this as stack starts numbering at 1
  4479. $this->setColor($opt['col'], true);
  4480. $this->setStrokeColor($opt['str'], true);
  4481. $this->addContent("\n" . $opt['lin']);
  4482. // $this->currentLineStyle = $opt['lin'];
  4483. } else {
  4484. $this->nStateStack++;
  4485. $this->stateStack[$this->nStateStack] = [
  4486. 'col' => $this->currentColor,
  4487. 'str' => $this->currentStrokeColor,
  4488. 'lin' => $this->currentLineStyle
  4489. ];
  4490. }
  4491. $this->save();
  4492. }
  4493. /**
  4494. * restore a previously saved state
  4495. *
  4496. * @param int $pageEnd
  4497. */
  4498. function restoreState($pageEnd = 0)
  4499. {
  4500. if (!$pageEnd) {
  4501. $n = $this->nStateStack;
  4502. $this->currentColor = $this->stateStack[$n]['col'];
  4503. $this->currentStrokeColor = $this->stateStack[$n]['str'];
  4504. $this->addContent("\n" . $this->stateStack[$n]['lin']);
  4505. $this->currentLineStyle = $this->stateStack[$n]['lin'];
  4506. $this->stateStack[$n] = null;
  4507. unset($this->stateStack[$n]);
  4508. $this->nStateStack--;
  4509. }
  4510. $this->restore();
  4511. }
  4512. /**
  4513. * make a loose object, the output will go into this object, until it is closed, then will revert to
  4514. * the current one.
  4515. * this object will not appear until it is included within a page.
  4516. * the function will return the object number
  4517. *
  4518. * @return int
  4519. */
  4520. function openObject()
  4521. {
  4522. $this->nStack++;
  4523. $this->stack[$this->nStack] = ['c' => $this->currentContents, 'p' => $this->currentPage];
  4524. // add a new object of the content type, to hold the data flow
  4525. $this->numObj++;
  4526. $this->o_contents($this->numObj, 'new');
  4527. $this->currentContents = $this->numObj;
  4528. $this->looseObjects[$this->numObj] = 1;
  4529. return $this->numObj;
  4530. }
  4531. /**
  4532. * open an existing object for editing
  4533. *
  4534. * @param $id
  4535. */
  4536. function reopenObject($id)
  4537. {
  4538. $this->nStack++;
  4539. $this->stack[$this->nStack] = ['c' => $this->currentContents, 'p' => $this->currentPage];
  4540. $this->currentContents = $id;
  4541. // also if this object is the primary contents for a page, then set the current page to its parent
  4542. if (isset($this->objects[$id]['onPage'])) {
  4543. $this->currentPage = $this->objects[$id]['onPage'];
  4544. }
  4545. }
  4546. /**
  4547. * close an object
  4548. */
  4549. function closeObject()
  4550. {
  4551. // close the object, as long as there was one open in the first place, which will be indicated by
  4552. // an objectId on the stack.
  4553. if ($this->nStack > 0) {
  4554. $this->currentContents = $this->stack[$this->nStack]['c'];
  4555. $this->currentPage = $this->stack[$this->nStack]['p'];
  4556. $this->nStack--;
  4557. // easier to probably not worry about removing the old entries, they will be overwritten
  4558. // if there are new ones.
  4559. }
  4560. }
  4561. /**
  4562. * stop an object from appearing on pages from this point on
  4563. *
  4564. * @param $id
  4565. */
  4566. function stopObject($id)
  4567. {
  4568. // if an object has been appearing on pages up to now, then stop it, this page will
  4569. // be the last one that could contain it.
  4570. if (isset($this->addLooseObjects[$id])) {
  4571. $this->addLooseObjects[$id] = '';
  4572. }
  4573. }
  4574. /**
  4575. * after an object has been created, it wil only show if it has been added, using this function.
  4576. *
  4577. * @param $id
  4578. * @param string $options
  4579. */
  4580. function addObject($id, $options = 'add')
  4581. {
  4582. // add the specified object to the page
  4583. if (isset($this->looseObjects[$id]) && $this->currentContents != $id) {
  4584. // then it is a valid object, and it is not being added to itself
  4585. switch ($options) {
  4586. case 'all':
  4587. // then this object is to be added to this page (done in the next block) and
  4588. // all future new pages.
  4589. $this->addLooseObjects[$id] = 'all';
  4590. case 'add':
  4591. if (isset($this->objects[$this->currentContents]['onPage'])) {
  4592. // then the destination contents is the primary for the page
  4593. // (though this object is actually added to that page)
  4594. $this->o_page($this->objects[$this->currentContents]['onPage'], 'content', $id);
  4595. }
  4596. break;
  4597. case 'even':
  4598. $this->addLooseObjects[$id] = 'even';
  4599. $pageObjectId = $this->objects[$this->currentContents]['onPage'];
  4600. if ($this->objects[$pageObjectId]['info']['pageNum'] % 2 == 0) {
  4601. $this->addObject($id);
  4602. // hacky huh :)
  4603. }
  4604. break;
  4605. case 'odd':
  4606. $this->addLooseObjects[$id] = 'odd';
  4607. $pageObjectId = $this->objects[$this->currentContents]['onPage'];
  4608. if ($this->objects[$pageObjectId]['info']['pageNum'] % 2 == 1) {
  4609. $this->addObject($id);
  4610. // hacky huh :)
  4611. }
  4612. break;
  4613. case 'next':
  4614. $this->addLooseObjects[$id] = 'all';
  4615. break;
  4616. case 'nexteven':
  4617. $this->addLooseObjects[$id] = 'even';
  4618. break;
  4619. case 'nextodd':
  4620. $this->addLooseObjects[$id] = 'odd';
  4621. break;
  4622. }
  4623. }
  4624. }
  4625. /**
  4626. * return a storable representation of a specific object
  4627. *
  4628. * @param $id
  4629. * @return string|null
  4630. */
  4631. function serializeObject($id)
  4632. {
  4633. if (array_key_exists($id, $this->objects)) {
  4634. return serialize($this->objects[$id]);
  4635. }
  4636. return null;
  4637. }
  4638. /**
  4639. * restore an object from its stored representation. Returns its new object id.
  4640. *
  4641. * @param $obj
  4642. * @return int
  4643. */
  4644. function restoreSerializedObject($obj)
  4645. {
  4646. $obj_id = $this->openObject();
  4647. $this->objects[$obj_id] = unserialize($obj);
  4648. $this->closeObject();
  4649. return $obj_id;
  4650. }
  4651. /**
  4652. * Embeds a file inside the PDF
  4653. *
  4654. * @param string $filepath path to the file to store inside the PDF
  4655. * @param string $embeddedFilename the filename displayed in the list of embedded files
  4656. * @param string $description a description in the list of embedded files
  4657. */
  4658. public function addEmbeddedFile(string $filepath, string $embeddedFilename, string $description): void
  4659. {
  4660. $this->numObj++;
  4661. $this->o_embedded_file_dictionary(
  4662. $this->numObj,
  4663. 'new',
  4664. [
  4665. 'filepath' => $filepath,
  4666. 'filename' => $embeddedFilename,
  4667. 'description' => $description
  4668. ]
  4669. );
  4670. }
  4671. /**
  4672. * add content to the documents info object
  4673. *
  4674. * @param $label
  4675. * @param int $value
  4676. */
  4677. function addInfo($label, $value = 0)
  4678. {
  4679. // this will only work if the label is one of the valid ones.
  4680. // modify this so that arrays can be passed as well.
  4681. // if $label is an array then assume that it is key => value pairs
  4682. // else assume that they are both scalar, anything else will probably error
  4683. if (is_array($label)) {
  4684. foreach ($label as $l => $v) {
  4685. $this->o_info($this->infoObject, $l, $v);
  4686. }
  4687. } else {
  4688. $this->o_info($this->infoObject, $label, $value);
  4689. }
  4690. }
  4691. /**
  4692. * set the viewer preferences of the document, it is up to the browser to obey these.
  4693. *
  4694. * @param $label
  4695. * @param int $value
  4696. */
  4697. function setPreferences($label, $value = 0)
  4698. {
  4699. // this will only work if the label is one of the valid ones.
  4700. if (is_array($label)) {
  4701. foreach ($label as $l => $v) {
  4702. $this->o_catalog($this->catalogId, 'viewerPreferences', [$l => $v]);
  4703. }
  4704. } else {
  4705. $this->o_catalog($this->catalogId, 'viewerPreferences', [$label => $value]);
  4706. }
  4707. }
  4708. /**
  4709. * extract an integer from a position in a byte stream
  4710. *
  4711. * @param $data
  4712. * @param $pos
  4713. * @param $num
  4714. * @return int
  4715. */
  4716. private function getBytes(&$data, $pos, $num)
  4717. {
  4718. // return the integer represented by $num bytes from $pos within $data
  4719. $ret = 0;
  4720. for ($i = 0; $i < $num; $i++) {
  4721. $ret *= 256;
  4722. $ret += ord($data[$pos + $i]);
  4723. }
  4724. return $ret;
  4725. }
  4726. /**
  4727. * Check if image already added to pdf image directory.
  4728. * If yes, need not to create again (pass empty data)
  4729. *
  4730. * @param $imgname
  4731. * @return bool
  4732. */
  4733. function image_iscached($imgname)
  4734. {
  4735. return isset($this->imagelist[$imgname]);
  4736. }
  4737. /**
  4738. * add a PNG image into the document, from a GD object
  4739. * this should work with remote files
  4740. *
  4741. * @param resource $img A GD resource
  4742. * @param string $file The PNG file
  4743. * @param float $x X position
  4744. * @param float $y Y position
  4745. * @param float $w Width
  4746. * @param float $h Height
  4747. * @param bool $is_mask true if the image is a mask
  4748. * @param bool $mask true if the image is masked
  4749. * @throws Exception
  4750. */
  4751. function addImagePng(&$img, $file, $x, $y, $w = 0.0, $h = 0.0, $is_mask = false, $mask = null)
  4752. {
  4753. if (!function_exists("imagepng")) {
  4754. throw new \Exception("The PHP GD extension is required, but is not installed.");
  4755. }
  4756. //if already cached, need not to read again
  4757. if (isset($this->imagelist[$file])) {
  4758. $data = null;
  4759. } else {
  4760. // Example for transparency handling on new image. Retain for current image
  4761. // $tIndex = imagecolortransparent($img);
  4762. // if ($tIndex > 0) {
  4763. // $tColor = imagecolorsforindex($img, $tIndex);
  4764. // $new_tIndex = imagecolorallocate($new_img, $tColor['red'], $tColor['green'], $tColor['blue']);
  4765. // imagefill($new_img, 0, 0, $new_tIndex);
  4766. // imagecolortransparent($new_img, $new_tIndex);
  4767. // }
  4768. // blending mode (literal/blending) on drawing into current image. not relevant when not saved or not drawn
  4769. //imagealphablending($img, true);
  4770. //default, but explicitely set to ensure pdf compatibility
  4771. imagesavealpha($img, false/*!$is_mask && !$mask*/);
  4772. $error = 0;
  4773. //DEBUG_IMG_TEMP
  4774. //debugpng
  4775. if (defined("DEBUGPNG") && DEBUGPNG) {
  4776. print '[addImagePng ' . $file . ']';
  4777. }
  4778. ob_start();
  4779. @imagepng($img);
  4780. $data = ob_get_clean();
  4781. if ($data == '') {
  4782. $error = 1;
  4783. $errormsg = 'trouble writing file from GD';
  4784. //DEBUG_IMG_TEMP
  4785. //debugpng
  4786. if (defined("DEBUGPNG") && DEBUGPNG) {
  4787. print 'trouble writing file from GD';
  4788. }
  4789. }
  4790. if ($error) {
  4791. $this->addMessage('PNG error - (' . $file . ') ' . $errormsg);
  4792. return;
  4793. }
  4794. } //End isset($this->imagelist[$file]) (png Duplicate removal)
  4795. $this->addPngFromBuf($data, $file, $x, $y, $w, $h, $is_mask, $mask);
  4796. }
  4797. /**
  4798. * @param $file
  4799. * @param $x
  4800. * @param $y
  4801. * @param $w
  4802. * @param $h
  4803. * @param $byte
  4804. */
  4805. protected function addImagePngAlpha($file, $x, $y, $w, $h, $byte)
  4806. {
  4807. // generate images
  4808. $img = imagecreatefrompng($file);
  4809. if ($img === false) {
  4810. return;
  4811. }
  4812. // FIXME The pixel transformation doesn't work well with 8bit PNGs
  4813. $eight_bit = ($byte & 4) !== 4;
  4814. $wpx = imagesx($img);
  4815. $hpx = imagesy($img);
  4816. imagesavealpha($img, false);
  4817. // create temp alpha file
  4818. $tempfile_alpha = @tempnam($this->tmp, "cpdf_img_");
  4819. @unlink($tempfile_alpha);
  4820. $tempfile_alpha = "$tempfile_alpha.png";
  4821. // create temp plain file
  4822. $tempfile_plain = @tempnam($this->tmp, "cpdf_img_");
  4823. @unlink($tempfile_plain);
  4824. $tempfile_plain = "$tempfile_plain.png";
  4825. $imgalpha = imagecreate($wpx, $hpx);
  4826. imagesavealpha($imgalpha, false);
  4827. // generate gray scale palette (0 -> 255)
  4828. for ($c = 0; $c < 256; ++$c) {
  4829. imagecolorallocate($imgalpha, $c, $c, $c);
  4830. }
  4831. // Use PECL gmagick + Graphics Magic to process transparent PNG images
  4832. if (extension_loaded("gmagick")) {
  4833. $gmagick = new \Gmagick($file);
  4834. $gmagick->setimageformat('png');
  4835. // Get opacity channel (negative of alpha channel)
  4836. $alpha_channel_neg = clone $gmagick;
  4837. $alpha_channel_neg->separateimagechannel(\Gmagick::CHANNEL_OPACITY);
  4838. // Negate opacity channel
  4839. $alpha_channel = new \Gmagick();
  4840. $alpha_channel->newimage($wpx, $hpx, "#FFFFFF", "png");
  4841. $alpha_channel->compositeimage($alpha_channel_neg, \Gmagick::COMPOSITE_DIFFERENCE, 0, 0);
  4842. $alpha_channel->separateimagechannel(\Gmagick::CHANNEL_RED);
  4843. $alpha_channel->writeimage($tempfile_alpha);
  4844. // Cast to 8bit+palette
  4845. $imgalpha_ = imagecreatefrompng($tempfile_alpha);
  4846. imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx);
  4847. imagedestroy($imgalpha_);
  4848. imagepng($imgalpha, $tempfile_alpha);
  4849. // Make opaque image
  4850. $color_channels = new \Gmagick();
  4851. $color_channels->newimage($wpx, $hpx, "#FFFFFF", "png");
  4852. $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYRED, 0, 0);
  4853. $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYGREEN, 0, 0);
  4854. $color_channels->compositeimage($gmagick, \Gmagick::COMPOSITE_COPYBLUE, 0, 0);
  4855. $color_channels->writeimage($tempfile_plain);
  4856. $imgplain = imagecreatefrompng($tempfile_plain);
  4857. }
  4858. // Use PECL imagick + ImageMagic to process transparent PNG images
  4859. elseif (extension_loaded("imagick")) {
  4860. // Native cloning was added to pecl-imagick in svn commit 263814
  4861. // the first version containing it was 3.0.1RC1
  4862. static $imagickClonable = null;
  4863. if ($imagickClonable === null) {
  4864. $imagickClonable = version_compare(\Imagick::IMAGICK_EXTVER, '3.0.1rc1') > 0;
  4865. }
  4866. $imagick = new \Imagick($file);
  4867. $imagick->setFormat('png');
  4868. // Get opacity channel (negative of alpha channel)
  4869. if ($imagick->getImageAlphaChannel() !== 0) {
  4870. $alpha_channel = $imagickClonable ? clone $imagick : $imagick->clone();
  4871. $alpha_channel->separateImageChannel(\Imagick::CHANNEL_ALPHA);
  4872. // Since ImageMagick7 negate invert transparency as default
  4873. if (\Imagick::getVersion()['versionNumber'] < 1800) {
  4874. $alpha_channel->negateImage(true);
  4875. }
  4876. $alpha_channel->writeImage($tempfile_alpha);
  4877. // Cast to 8bit+palette
  4878. $imgalpha_ = imagecreatefrompng($tempfile_alpha);
  4879. imagecopy($imgalpha, $imgalpha_, 0, 0, 0, 0, $wpx, $hpx);
  4880. imagedestroy($imgalpha_);
  4881. imagepng($imgalpha, $tempfile_alpha);
  4882. } else {
  4883. $tempfile_alpha = null;
  4884. }
  4885. // Make opaque image
  4886. $color_channels = new \Imagick();
  4887. $color_channels->newImage($wpx, $hpx, "#FFFFFF", "png");
  4888. $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYRED, 0, 0);
  4889. $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYGREEN, 0, 0);
  4890. $color_channels->compositeImage($imagick, \Imagick::COMPOSITE_COPYBLUE, 0, 0);
  4891. $color_channels->writeImage($tempfile_plain);
  4892. $imgplain = imagecreatefrompng($tempfile_plain);
  4893. } else {
  4894. // allocated colors cache
  4895. $allocated_colors = [];
  4896. // extract alpha channel
  4897. for ($xpx = 0; $xpx < $wpx; ++$xpx) {
  4898. for ($ypx = 0; $ypx < $hpx; ++$ypx) {
  4899. $color = imagecolorat($img, $xpx, $ypx);
  4900. $col = imagecolorsforindex($img, $color);
  4901. $alpha = $col['alpha'];
  4902. if ($eight_bit) {
  4903. // with gamma correction
  4904. $gammacorr = 2.2;
  4905. $pixel = pow((((127 - $alpha) * 255 / 127) / 255), $gammacorr) * 255;
  4906. } else {
  4907. // without gamma correction
  4908. $pixel = (127 - $alpha) * 2;
  4909. $key = $col['red'] . $col['green'] . $col['blue'];
  4910. if (!isset($allocated_colors[$key])) {
  4911. $pixel_img = imagecolorallocate($img, $col['red'], $col['green'], $col['blue']);
  4912. $allocated_colors[$key] = $pixel_img;
  4913. } else {
  4914. $pixel_img = $allocated_colors[$key];
  4915. }
  4916. imagesetpixel($img, $xpx, $ypx, $pixel_img);
  4917. }
  4918. imagesetpixel($imgalpha, $xpx, $ypx, $pixel);
  4919. }
  4920. }
  4921. // extract image without alpha channel
  4922. $imgplain = imagecreatetruecolor($wpx, $hpx);
  4923. imagecopy($imgplain, $img, 0, 0, 0, 0, $wpx, $hpx);
  4924. imagedestroy($img);
  4925. imagepng($imgalpha, $tempfile_alpha);
  4926. imagepng($imgplain, $tempfile_plain);
  4927. }
  4928. // embed mask image
  4929. if ($tempfile_alpha) {
  4930. $this->addImagePng($imgalpha, $tempfile_alpha, $x, $y, $w, $h, true);
  4931. imagedestroy($imgalpha);
  4932. }
  4933. // embed image, masked with previously embedded mask
  4934. $this->addImagePng($imgplain, $tempfile_plain, $x, $y, $w, $h, false, ($tempfile_alpha !== null));
  4935. imagedestroy($imgplain);
  4936. // remove temp files
  4937. if ($tempfile_alpha) {
  4938. unlink($tempfile_alpha);
  4939. }
  4940. unlink($tempfile_plain);
  4941. }
  4942. /**
  4943. * add a PNG image into the document, from a file
  4944. * this should work with remote files
  4945. *
  4946. * @param $file
  4947. * @param $x
  4948. * @param $y
  4949. * @param int $w
  4950. * @param int $h
  4951. * @throws Exception
  4952. */
  4953. function addPngFromFile($file, $x, $y, $w = 0, $h = 0)
  4954. {
  4955. if (!function_exists("imagecreatefrompng")) {
  4956. throw new \Exception("The PHP GD extension is required, but is not installed.");
  4957. }
  4958. //if already cached, need not to read again
  4959. if (isset($this->imagelist[$file])) {
  4960. $img = null;
  4961. } else {
  4962. $info = file_get_contents($file, false, null, 24, 5);
  4963. $meta = unpack("CbitDepth/CcolorType/CcompressionMethod/CfilterMethod/CinterlaceMethod", $info);
  4964. $bit_depth = $meta["bitDepth"];
  4965. $color_type = $meta["colorType"];
  4966. // http://www.w3.org/TR/PNG/#11IHDR
  4967. // 3 => indexed
  4968. // 4 => greyscale with alpha
  4969. // 6 => fullcolor with alpha
  4970. $is_alpha = in_array($color_type, [4, 6]) || ($color_type == 3 && $bit_depth != 4);
  4971. if ($is_alpha) { // exclude grayscale alpha
  4972. $this->addImagePngAlpha($file, $x, $y, $w, $h, $color_type);
  4973. return;
  4974. }
  4975. //png files typically contain an alpha channel.
  4976. //pdf file format or class.pdf does not support alpha blending.
  4977. //on alpha blended images, more transparent areas have a color near black.
  4978. //This appears in the result on not storing the alpha channel.
  4979. //Correct would be the box background image or its parent when transparent.
  4980. //But this would make the image dependent on the background.
  4981. //Therefore create an image with white background and copy in
  4982. //A more natural background than black is white.
  4983. //Therefore create an empty image with white background and merge the
  4984. //image in with alpha blending.
  4985. $imgtmp = @imagecreatefrompng($file);
  4986. if (!$imgtmp) {
  4987. return;
  4988. }
  4989. $sx = imagesx($imgtmp);
  4990. $sy = imagesy($imgtmp);
  4991. $img = imagecreatetruecolor($sx, $sy);
  4992. imagealphablending($img, true);
  4993. // @todo is it still needed ??
  4994. $ti = imagecolortransparent($imgtmp);
  4995. if ($ti >= 0) {
  4996. $tc = imagecolorsforindex($imgtmp, $ti);
  4997. $ti = imagecolorallocate($img, $tc['red'], $tc['green'], $tc['blue']);
  4998. imagefill($img, 0, 0, $ti);
  4999. imagecolortransparent($img, $ti);
  5000. } else {
  5001. imagefill($img, 1, 1, imagecolorallocate($img, 255, 255, 255));
  5002. }
  5003. imagecopy($img, $imgtmp, 0, 0, 0, 0, $sx, $sy);
  5004. imagedestroy($imgtmp);
  5005. }
  5006. $this->addImagePng($img, $file, $x, $y, $w, $h);
  5007. if ($img) {
  5008. imagedestroy($img);
  5009. }
  5010. }
  5011. /**
  5012. * add a PNG image into the document, from a file
  5013. * this should work with remote files
  5014. *
  5015. * @param $file
  5016. * @param $x
  5017. * @param $y
  5018. * @param int $w
  5019. * @param int $h
  5020. */
  5021. function addSvgFromFile($file, $x, $y, $w = 0, $h = 0)
  5022. {
  5023. $doc = new \Svg\Document();
  5024. $doc->loadFile($file);
  5025. $dimensions = $doc->getDimensions();
  5026. $this->save();
  5027. $this->transform([$w / $dimensions["width"], 0, 0, $h / $dimensions["height"], $x, $y]);
  5028. $surface = new \Svg\Surface\SurfaceCpdf($doc, $this);
  5029. $doc->render($surface);
  5030. $this->restore();
  5031. }
  5032. /**
  5033. * add a PNG image into the document, from a memory buffer of the file
  5034. *
  5035. * @param $data
  5036. * @param $file
  5037. * @param $x
  5038. * @param $y
  5039. * @param float $w
  5040. * @param float $h
  5041. * @param bool $is_mask
  5042. * @param null $mask
  5043. */
  5044. function addPngFromBuf(&$data, $file, $x, $y, $w = 0.0, $h = 0.0, $is_mask = false, $mask = null)
  5045. {
  5046. if (isset($this->imagelist[$file])) {
  5047. $data = null;
  5048. $info['width'] = $this->imagelist[$file]['w'];
  5049. $info['height'] = $this->imagelist[$file]['h'];
  5050. $label = $this->imagelist[$file]['label'];
  5051. } else {
  5052. if ($data == null) {
  5053. $this->addMessage('addPngFromBuf error - data not present!');
  5054. return;
  5055. }
  5056. $error = 0;
  5057. if (!$error) {
  5058. $header = chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10);
  5059. if (mb_substr($data, 0, 8, '8bit') != $header) {
  5060. $error = 1;
  5061. if (defined("DEBUGPNG") && DEBUGPNG) {
  5062. print '[addPngFromFile this file does not have a valid header ' . $file . ']';
  5063. }
  5064. $errormsg = 'this file does not have a valid header';
  5065. }
  5066. }
  5067. if (!$error) {
  5068. // set pointer
  5069. $p = 8;
  5070. $len = mb_strlen($data, '8bit');
  5071. // cycle through the file, identifying chunks
  5072. $haveHeader = 0;
  5073. $info = [];
  5074. $idata = '';
  5075. $pdata = '';
  5076. while ($p < $len) {
  5077. $chunkLen = $this->getBytes($data, $p, 4);
  5078. $chunkType = mb_substr($data, $p + 4, 4, '8bit');
  5079. switch ($chunkType) {
  5080. case 'IHDR':
  5081. // this is where all the file information comes from
  5082. $info['width'] = $this->getBytes($data, $p + 8, 4);
  5083. $info['height'] = $this->getBytes($data, $p + 12, 4);
  5084. $info['bitDepth'] = ord($data[$p + 16]);
  5085. $info['colorType'] = ord($data[$p + 17]);
  5086. $info['compressionMethod'] = ord($data[$p + 18]);
  5087. $info['filterMethod'] = ord($data[$p + 19]);
  5088. $info['interlaceMethod'] = ord($data[$p + 20]);
  5089. //print_r($info);
  5090. $haveHeader = 1;
  5091. if ($info['compressionMethod'] != 0) {
  5092. $error = 1;
  5093. //debugpng
  5094. if (defined("DEBUGPNG") && DEBUGPNG) {
  5095. print '[addPngFromFile unsupported compression method ' . $file . ']';
  5096. }
  5097. $errormsg = 'unsupported compression method';
  5098. }
  5099. if ($info['filterMethod'] != 0) {
  5100. $error = 1;
  5101. //debugpng
  5102. if (defined("DEBUGPNG") && DEBUGPNG) {
  5103. print '[addPngFromFile unsupported filter method ' . $file . ']';
  5104. }
  5105. $errormsg = 'unsupported filter method';
  5106. }
  5107. break;
  5108. case 'PLTE':
  5109. $pdata .= mb_substr($data, $p + 8, $chunkLen, '8bit');
  5110. break;
  5111. case 'IDAT':
  5112. $idata .= mb_substr($data, $p + 8, $chunkLen, '8bit');
  5113. break;
  5114. case 'tRNS':
  5115. //this chunk can only occur once and it must occur after the PLTE chunk and before IDAT chunk
  5116. //print "tRNS found, color type = ".$info['colorType']."\n";
  5117. $transparency = [];
  5118. switch ($info['colorType']) {
  5119. // indexed color, rbg
  5120. case 3:
  5121. /* corresponding to entries in the plte chunk
  5122. Alpha for palette index 0: 1 byte
  5123. Alpha for palette index 1: 1 byte
  5124. ...etc...
  5125. */
  5126. // there will be one entry for each palette entry. up until the last non-opaque entry.
  5127. // set up an array, stretching over all palette entries which will be o (opaque) or 1 (transparent)
  5128. $transparency['type'] = 'indexed';
  5129. $trans = 0;
  5130. for ($i = $chunkLen; $i >= 0; $i--) {
  5131. if (ord($data[$p + 8 + $i]) == 0) {
  5132. $trans = $i;
  5133. }
  5134. }
  5135. $transparency['data'] = $trans;
  5136. break;
  5137. // grayscale
  5138. case 0:
  5139. /* corresponding to entries in the plte chunk
  5140. Gray: 2 bytes, range 0 .. (2^bitdepth)-1
  5141. */
  5142. // $transparency['grayscale'] = $this->PRVT_getBytes($data,$p+8,2); // g = grayscale
  5143. $transparency['type'] = 'indexed';
  5144. $transparency['data'] = ord($data[$p + 8 + 1]);
  5145. break;
  5146. // truecolor
  5147. case 2:
  5148. /* corresponding to entries in the plte chunk
  5149. Red: 2 bytes, range 0 .. (2^bitdepth)-1
  5150. Green: 2 bytes, range 0 .. (2^bitdepth)-1
  5151. Blue: 2 bytes, range 0 .. (2^bitdepth)-1
  5152. */
  5153. $transparency['r'] = $this->getBytes($data, $p + 8, 2);
  5154. // r from truecolor
  5155. $transparency['g'] = $this->getBytes($data, $p + 10, 2);
  5156. // g from truecolor
  5157. $transparency['b'] = $this->getBytes($data, $p + 12, 2);
  5158. // b from truecolor
  5159. $transparency['type'] = 'color-key';
  5160. break;
  5161. //unsupported transparency type
  5162. default:
  5163. if (defined("DEBUGPNG") && DEBUGPNG) {
  5164. print '[addPngFromFile unsupported transparency type ' . $file . ']';
  5165. }
  5166. break;
  5167. }
  5168. // KS End new code
  5169. break;
  5170. default:
  5171. break;
  5172. }
  5173. $p += $chunkLen + 12;
  5174. }
  5175. if (!$haveHeader) {
  5176. $error = 1;
  5177. //debugpng
  5178. if (defined("DEBUGPNG") && DEBUGPNG) {
  5179. print '[addPngFromFile information header is missing ' . $file . ']';
  5180. }
  5181. $errormsg = 'information header is missing';
  5182. }
  5183. if (isset($info['interlaceMethod']) && $info['interlaceMethod']) {
  5184. $error = 1;
  5185. //debugpng
  5186. if (defined("DEBUGPNG") && DEBUGPNG) {
  5187. print '[addPngFromFile no support for interlaced images in pdf ' . $file . ']';
  5188. }
  5189. $errormsg = 'There appears to be no support for interlaced images in pdf.';
  5190. }
  5191. }
  5192. if (!$error && $info['bitDepth'] > 8) {
  5193. $error = 1;
  5194. //debugpng
  5195. if (defined("DEBUGPNG") && DEBUGPNG) {
  5196. print '[addPngFromFile bit depth of 8 or less is supported ' . $file . ']';
  5197. }
  5198. $errormsg = 'only bit depth of 8 or less is supported';
  5199. }
  5200. if (!$error) {
  5201. switch ($info['colorType']) {
  5202. case 3:
  5203. $color = 'DeviceRGB';
  5204. $ncolor = 1;
  5205. break;
  5206. case 2:
  5207. $color = 'DeviceRGB';
  5208. $ncolor = 3;
  5209. break;
  5210. case 0:
  5211. $color = 'DeviceGray';
  5212. $ncolor = 1;
  5213. break;
  5214. default:
  5215. $error = 1;
  5216. //debugpng
  5217. if (defined("DEBUGPNG") && DEBUGPNG) {
  5218. print '[addPngFromFile alpha channel not supported: ' . $info['colorType'] . ' ' . $file . ']';
  5219. }
  5220. $errormsg = 'transparency alpha channel not supported, transparency only supported for palette images.';
  5221. }
  5222. }
  5223. if ($error) {
  5224. $this->addMessage('PNG error - (' . $file . ') ' . $errormsg);
  5225. return;
  5226. }
  5227. //print_r($info);
  5228. // so this image is ok... add it in.
  5229. $this->numImages++;
  5230. $im = $this->numImages;
  5231. $label = "I$im";
  5232. $this->numObj++;
  5233. // $this->o_image($this->numObj,'new',array('label' => $label,'data' => $idata,'iw' => $w,'ih' => $h,'type' => 'png','ic' => $info['width']));
  5234. $options = [
  5235. 'label' => $label,
  5236. 'data' => $idata,
  5237. 'bitsPerComponent' => $info['bitDepth'],
  5238. 'pdata' => $pdata,
  5239. 'iw' => $info['width'],
  5240. 'ih' => $info['height'],
  5241. 'type' => 'png',
  5242. 'color' => $color,
  5243. 'ncolor' => $ncolor,
  5244. 'masked' => $mask,
  5245. 'isMask' => $is_mask
  5246. ];
  5247. if (isset($transparency)) {
  5248. $options['transparency'] = $transparency;
  5249. }
  5250. $this->o_image($this->numObj, 'new', $options);
  5251. $this->imagelist[$file] = ['label' => $label, 'w' => $info['width'], 'h' => $info['height']];
  5252. }
  5253. if ($is_mask) {
  5254. return;
  5255. }
  5256. if ($w <= 0 && $h <= 0) {
  5257. $w = $info['width'];
  5258. $h = $info['height'];
  5259. }
  5260. if ($w <= 0) {
  5261. $w = $h / $info['height'] * $info['width'];
  5262. }
  5263. if ($h <= 0) {
  5264. $h = $w * $info['height'] / $info['width'];
  5265. }
  5266. $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ", $w, $h, $x, $y, $label));
  5267. }
  5268. /**
  5269. * add a JPEG image into the document, from a file
  5270. *
  5271. * @param $img
  5272. * @param $x
  5273. * @param $y
  5274. * @param int $w
  5275. * @param int $h
  5276. */
  5277. function addJpegFromFile($img, $x, $y, $w = 0, $h = 0)
  5278. {
  5279. // attempt to add a jpeg image straight from a file, using no GD commands
  5280. // note that this function is unable to operate on a remote file.
  5281. if (!file_exists($img)) {
  5282. return;
  5283. }
  5284. if ($this->image_iscached($img)) {
  5285. $data = null;
  5286. $imageWidth = $this->imagelist[$img]['w'];
  5287. $imageHeight = $this->imagelist[$img]['h'];
  5288. $channels = $this->imagelist[$img]['c'];
  5289. } else {
  5290. $tmp = getimagesize($img);
  5291. $imageWidth = $tmp[0];
  5292. $imageHeight = $tmp[1];
  5293. if (isset($tmp['channels'])) {
  5294. $channels = $tmp['channels'];
  5295. } else {
  5296. $channels = 3;
  5297. }
  5298. $data = file_get_contents($img);
  5299. }
  5300. if ($w <= 0 && $h <= 0) {
  5301. $w = $imageWidth;
  5302. }
  5303. if ($w == 0) {
  5304. $w = $h / $imageHeight * $imageWidth;
  5305. }
  5306. if ($h == 0) {
  5307. $h = $w * $imageHeight / $imageWidth;
  5308. }
  5309. $this->addJpegImage_common($data, $img, $imageWidth, $imageHeight, $x, $y, $w, $h, $channels);
  5310. }
  5311. /**
  5312. * common code used by the two JPEG adding functions
  5313. * @param $data
  5314. * @param $imgname
  5315. * @param $imageWidth
  5316. * @param $imageHeight
  5317. * @param $x
  5318. * @param $y
  5319. * @param int $w
  5320. * @param int $h
  5321. * @param int $channels
  5322. */
  5323. private function addJpegImage_common(
  5324. &$data,
  5325. $imgname,
  5326. $imageWidth,
  5327. $imageHeight,
  5328. $x,
  5329. $y,
  5330. $w = 0,
  5331. $h = 0,
  5332. $channels = 3
  5333. ) {
  5334. if ($this->image_iscached($imgname)) {
  5335. $label = $this->imagelist[$imgname]['label'];
  5336. //debugpng
  5337. //if (DEBUGPNG) print '[addJpegImage_common Duplicate '.$imgname.']';
  5338. } else {
  5339. if ($data == null) {
  5340. $this->addMessage('addJpegImage_common error - (' . $imgname . ') data not present!');
  5341. return;
  5342. }
  5343. // note that this function is not to be called externally
  5344. // it is just the common code between the GD and the file options
  5345. $this->numImages++;
  5346. $im = $this->numImages;
  5347. $label = "I$im";
  5348. $this->numObj++;
  5349. $this->o_image(
  5350. $this->numObj,
  5351. 'new',
  5352. [
  5353. 'label' => $label,
  5354. 'data' => &$data,
  5355. 'iw' => $imageWidth,
  5356. 'ih' => $imageHeight,
  5357. 'channels' => $channels
  5358. ]
  5359. );
  5360. $this->imagelist[$imgname] = [
  5361. 'label' => $label,
  5362. 'w' => $imageWidth,
  5363. 'h' => $imageHeight,
  5364. 'c' => $channels
  5365. ];
  5366. }
  5367. $this->addContent(sprintf("\nq\n%.3F 0 0 %.3F %.3F %.3F cm /%s Do\nQ ", $w, $h, $x, $y, $label));
  5368. }
  5369. /**
  5370. * specify where the document should open when it first starts
  5371. *
  5372. * @param $style
  5373. * @param int $a
  5374. * @param int $b
  5375. * @param int $c
  5376. */
  5377. function openHere($style, $a = 0, $b = 0, $c = 0)
  5378. {
  5379. // this function will open the document at a specified page, in a specified style
  5380. // the values for style, and the required parameters are:
  5381. // 'XYZ' left, top, zoom
  5382. // 'Fit'
  5383. // 'FitH' top
  5384. // 'FitV' left
  5385. // 'FitR' left,bottom,right
  5386. // 'FitB'
  5387. // 'FitBH' top
  5388. // 'FitBV' left
  5389. $this->numObj++;
  5390. $this->o_destination(
  5391. $this->numObj,
  5392. 'new',
  5393. ['page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c]
  5394. );
  5395. $id = $this->catalogId;
  5396. $this->o_catalog($id, 'openHere', $this->numObj);
  5397. }
  5398. /**
  5399. * Add JavaScript code to the PDF document
  5400. *
  5401. * @param string $code
  5402. */
  5403. function addJavascript($code)
  5404. {
  5405. $this->javascript .= $code;
  5406. }
  5407. /**
  5408. * create a labelled destination within the document
  5409. *
  5410. * @param $label
  5411. * @param $style
  5412. * @param int $a
  5413. * @param int $b
  5414. * @param int $c
  5415. */
  5416. function addDestination($label, $style, $a = 0, $b = 0, $c = 0)
  5417. {
  5418. // associates the given label with the destination, it is done this way so that a destination can be specified after
  5419. // it has been linked to
  5420. // styles are the same as the 'openHere' function
  5421. $this->numObj++;
  5422. $this->o_destination(
  5423. $this->numObj,
  5424. 'new',
  5425. ['page' => $this->currentPage, 'type' => $style, 'p1' => $a, 'p2' => $b, 'p3' => $c]
  5426. );
  5427. $id = $this->numObj;
  5428. // store the label->idf relationship, note that this means that labels can be used only once
  5429. $this->destinations["$label"] = $id;
  5430. }
  5431. /**
  5432. * define font families, this is used to initialize the font families for the default fonts
  5433. * and for the user to add new ones for their fonts. The default bahavious can be overridden should
  5434. * that be desired.
  5435. *
  5436. * @param $family
  5437. * @param string $options
  5438. */
  5439. function setFontFamily($family, $options = '')
  5440. {
  5441. if (!is_array($options)) {
  5442. if ($family === 'init') {
  5443. // set the known family groups
  5444. // these font families will be used to enable bold and italic markers to be included
  5445. // within text streams. html forms will be used... <b></b> <i></i>
  5446. $this->fontFamilies['Helvetica.afm'] =
  5447. [
  5448. 'b' => 'Helvetica-Bold.afm',
  5449. 'i' => 'Helvetica-Oblique.afm',
  5450. 'bi' => 'Helvetica-BoldOblique.afm',
  5451. 'ib' => 'Helvetica-BoldOblique.afm'
  5452. ];
  5453. $this->fontFamilies['Courier.afm'] =
  5454. [
  5455. 'b' => 'Courier-Bold.afm',
  5456. 'i' => 'Courier-Oblique.afm',
  5457. 'bi' => 'Courier-BoldOblique.afm',
  5458. 'ib' => 'Courier-BoldOblique.afm'
  5459. ];
  5460. $this->fontFamilies['Times-Roman.afm'] =
  5461. [
  5462. 'b' => 'Times-Bold.afm',
  5463. 'i' => 'Times-Italic.afm',
  5464. 'bi' => 'Times-BoldItalic.afm',
  5465. 'ib' => 'Times-BoldItalic.afm'
  5466. ];
  5467. }
  5468. } else {
  5469. // the user is trying to set a font family
  5470. // note that this can also be used to set the base ones to something else
  5471. if (mb_strlen($family)) {
  5472. $this->fontFamilies[$family] = $options;
  5473. }
  5474. }
  5475. }
  5476. /**
  5477. * used to add messages for use in debugging
  5478. *
  5479. * @param $message
  5480. */
  5481. function addMessage($message)
  5482. {
  5483. $this->messages .= $message . "\n";
  5484. }
  5485. /**
  5486. * a few functions which should allow the document to be treated transactionally.
  5487. *
  5488. * @param $action
  5489. */
  5490. function transaction($action)
  5491. {
  5492. switch ($action) {
  5493. case 'start':
  5494. // store all the data away into the checkpoint variable
  5495. $data = get_object_vars($this);
  5496. $this->checkpoint = $data;
  5497. unset($data);
  5498. break;
  5499. case 'commit':
  5500. if (is_array($this->checkpoint) && isset($this->checkpoint['checkpoint'])) {
  5501. $tmp = $this->checkpoint['checkpoint'];
  5502. $this->checkpoint = $tmp;
  5503. unset($tmp);
  5504. } else {
  5505. $this->checkpoint = '';
  5506. }
  5507. break;
  5508. case 'rewind':
  5509. // do not destroy the current checkpoint, but move us back to the state then, so that we can try again
  5510. if (is_array($this->checkpoint)) {
  5511. // can only abort if were inside a checkpoint
  5512. $tmp = $this->checkpoint;
  5513. foreach ($tmp as $k => $v) {
  5514. if ($k !== 'checkpoint') {
  5515. $this->$k = $v;
  5516. }
  5517. }
  5518. unset($tmp);
  5519. }
  5520. break;
  5521. case 'abort':
  5522. if (is_array($this->checkpoint)) {
  5523. // can only abort if were inside a checkpoint
  5524. $tmp = $this->checkpoint;
  5525. foreach ($tmp as $k => $v) {
  5526. $this->$k = $v;
  5527. }
  5528. unset($tmp);
  5529. }
  5530. break;
  5531. }
  5532. }
  5533. }