Att dra ut (Pulling) text, bilder och teckensnitt (fonts) ur en existerande PDF låter som ett löst problem tills du kör en riktig korpus (corpus) genom det. Rikta en sökindexerare (search indexer) mot fyrtiotusen kundfiler och det som går sönder (breakage) sorteras in i ett fåtal igenkännliga högar (recognizeable piles). Ord flyter ihop (run together) eftersom ingen berättade för extraktorn (extractor) hur brett ett mellanrum (gap) som räknas som ett mellanslag (space). Andra sidor kommer tillbaka som rotvälska (gibberish) eftersom ett delmängdsteckensnitt (subsetted font) inte bär på någon karta från sina glyfkoder (glyph codes) till faktiska tecken. Och "företagslogotypen" visar sig vara nio separata bildobjekt staplade bakom en mjuk mask (soft mask). Inget av det är en bugg (bug) i biblioteket. Det är skillnaden mellan att anropa en extraktionsfunktion och att förstå vad funktionen kan och inte kan återvinna (recover) från byten på disken
losLab PDF Library, Pascal-utgåvan, ger Delphi- och C++Builder-kod mer än ett sätt att läsa var och en av dessa tre strömmar (streams), och nivåerna skiljer sig (differ) i vad de garanterar. Tricket är att matcha nivån mot (to) jobbet: ett sökindex (search index), en redigeringsgranskare (redaction reviewer) och ett PDF/A-preflight-pass (preflight pass) vill alla ha olika saker ur (out of) samma sida, och att sträcka sig (reaching) efter fel anrop slösar möda eller producerar utdata du inte kan lita på
Extraktionsnivåer för text (Text extraction levels) och vad var och en lovar
GetPageText tar ett alternativsvärde (options value) från 0 till och med (through) 8, och det numret väljer en motor snarare än ett format. Värdena 0 till och med 2 kör ett lättviktspass (lightweight pass) som duger (is fine) för en snabb förhandsgranskning. Värdena 3 till och med 8 dirigerar (route) genom den layout-medvetna motorn, vilken bygger om (rebuilds) rader och avstånd från var glyferna faktiskt sitter på sidan. Inom det spannet har variationerna betydelse: 4 och 6 delar upp (split) utdata i ord, 5 och 6 sänder ut (emit) per-glyf-bredder, och 7 returnerar ren text (plain text) med teckensnitts-, färg- och block-metadata medvetet droppad. Alternativ 7 är det man ska mata (feed) ett sökindex med, eftersom indexet vill ha ord och ingenting annat
Inget alternativ-värde (option setting) kan rädda (rescue) ett dokument som aldrig bar informationen från första början (to begin with). PDF mappar teckenkoder till glyf-former (glyph shapes), och det enda som mappar de där koderna tillbaka till läsbar text är ett teckensnitts ToUnicode CMap (ISO 32000-1 §9.10). När ett delmängdsteckensnitt skeppas (ships) utan en (without one), sitter varje extraktor fast (stuck). Det här biblioteket, kopiera-klistra in i en visare, en konkurrerande verktygslåda (competing toolkit): alla är de reducerade till att gissa (guessing) utifrån (from) glyf-namn eller returnera ingenting. Det praktiska gensvaret (response) är upptäckt (detection), inte hjältedåd (heroics). Poängsätt (Score) sidan som låg konfidens (low-confidence) och skicka den till OCR, eftersom att indexera skräpet i tysthet är värre än att erkänna (admitting) att du inte kan läsa det
För de fall (cases) de platta (flat) alternativen inte täcker (cover) – anpassad tokenisering (custom tokenization), innehållsströms-kriminalteknik (content-stream forensics), en texttratt (text funnel) byggd efter dina egna regler – är avkodaren (decoder) tillgänglig ett lager ner. TPDFExtractor konstrueras (is constructed) över en sidas resursordbok (resources dictionary) och teckensnittssamling (font collection). Dess ExtractTextW-metod kör (runs) råa (raw) innehållsströms-text-operationer (content-stream text operations) tillbaka genom samma teckensnitts-maskineri för att återvinna (recover) Unicode, och dess OnFindObject-händelse räcker (hands) dig varje objekt allteftersom (as) det strömmar förbi. Den mesta kod behöver aldrig sträcka sig så här djupt (reach this deep). Applikationerna som gör det är de som är glada att lagret är publikt snarare än begravt
Positionerade block: enheten för sökträffar (search hits) och redigerings-granskning (redaction review)
Ren text talar om för dig vad sidan säger. Förr eller senare (Sooner or later) behöver en produkt också veta var (where) den säger det, så att den kan markera (highlight) en sökträff, rita en ruta (box) runt en redigerings-kandidat (redaction candidate), eller ankra (anchor) en anteckning (annotation) på rätt fläck (spot). ExtractPageTextBlocks returnerar ett handtag (handle) till en lista av textflöden (text runs), och varje flöde (run) bär sin text, sin begränsningsruta (bounding box) samt det teckensnittsnamn (font name) och den storlek det var satt (set) i:
var
Pdf: TPDFlib;
Blocks, I: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
raise Exception.Create('load failed');
Pdf.SelectPage(1);
Blocks := Pdf.ExtractPageTextBlocks(0);
for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
Writeln(Format('%s [%s %.1f pt at %.0f,%.0f]',
[Pdf.GetTextBlockText(Blocks, I),
Pdf.GetTextBlockFontName(Blocks, I),
Pdf.GetTextBlockFontSize(Blocks, I),
Pdf.GetTextBlockBound(Blocks, I, 0),
Pdf.GetTextBlockBound(Blocks, I, 1)]));
Pdf.ReleaseTextBlocks(Blocks);
finally
Pdf.Free;
end;
end;
En detalj på det här området får integreringar att snubbla (trips up) mer än någon annan. SetTextExtractionArea, SetTextExtractionWordGap och SetTextExtractionOptions är tillstånd (state) på dokumentnivå som kvarstår (persists), inte argument du skickar med (pass) per anrop. Konfigurera en arearestriktion för en funktion, låt säga (say) att läsa endast sidhuvud-bandet (header band) för att klassificera (classify) ett dokument, och det trunkerar (truncates) i tysthet varje extraktion som följer på samma handtag, inklusive de layout-medvetna GetPageText-nivåer du sträcker dig efter senare. Antingen (Either) återställer (reset) du extraktions-tillståndet mellan logiska uppgifter eller så ger du varje uppgift sitt eget dokument-handtag
Ordmellanrums-tröskeln (word-gap threshold) är spaken (lever) för den där första felhögen, orden som flyter ihop (run together). SetTextExtractionWordGap talar om för layout-motorn hur mycket horisontellt utrymme, mätt (measured) mot sidans eget glyf-avstånd (glyph spacing), som separerar ett ord från nästa. En tät (dense) tabell vill ha ett mindre mellanrum än en löst satt marknadsföringssida, så en tröskel avstämd (tuned) per dokumentklass slår en enda global konstant (global constant). Den kvarstår på dokumentet som resten av extraktionstillståndet, så planera att ställa in den medvetet snarare än att göra det en gång och glömma det (once and forget it)
Bilder: ursprungliga strömmar (original streams), inte skärmdumpar
Fel sätt att få ut bilder ur en PDF är att rendera sidan och beskära den. Det samplar om (resamples) pixlarna, bakar in (bakes in) eventuell (any) rotation och kastar bort (throws away) vad än originalet var. GetPageImageList räknar i stället upp (enumerates) de faktiska bildresurser som sidan refererar till, och varje objekt räcker tillbaka dess egenskaper och dess ursprungliga (original), ostörda (undisturbed) data:
var
ImgList, I: Integer;
begin
Pdf.SelectPage(1);
ImgList := Pdf.GetPageImageList(0);
for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
begin
Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
Format('page1-img%.2d.bin', [I]));
end;
Pdf.ReleaseImageList(ImgList);
end;
Kontrollera (Check) GetImageListItemFormatDesc innan du antar (assume) någonting om ett objekt, eftersom det en sida refererar till sällan är (rarely is) en städad (tidy) bild per synlig bild. En mjuk mask (soft mask) dyker upp som en alldeles egen, separat post (entry). Samma XObject repeterar (repeats) ofta tvärs över (across) många sidor, så deduplicera (deduplicate) via innehålls-hash innan du arkiverar (archive) en "alla bilder"-export, annars (or) kommer du att skriva samma logotyp hundra gånger. CMYK-JPEG-filer behöver färghantering (color management) applicerad nedströms (downstream), annars renderas de inverterade (inverted) i visare (viewers) som tar kanalerna för nominellt värde (face value). När du vill ha en dokument-täckande inventering (document-wide inventory) i stället för en sida i taget, skannar (scans) FindImages tillsammans med SetFindImagesMode hela filen i ett svep (one pass)
Det finns en gräns (boundary) värd att lyfta (raising) med intressenter (stakeholders) innan någon skriver acceptanskriterier (acceptance criteria): bild-extraktion returnerar enbart rasterresurser (raster resources). En logotyp eller ett diagram ritat som vektorbanor (vector paths) är inte en bild i resurs-bemärkelse (resource sense) och kommer aldrig att dyka upp (turn up) i någon bildlista, hur (no matter how) tydligt den än läses (reads) som en bild på skärmen. När kravet (requirement) verkligen är att leverera (deliver) det där diagrammet som en fil, är det ärliga (honest) tillvägagångssättet (approach) att rendera sidregionen (page region) till en bitmapp, vilket är en annorlunda operation med annorlunda trohet (fidelity). De två typerna av utdata hör inte hemma (do not belong) i samma export-mapp utan en etikett (label) som säger (saying) vad som är vad
Teckensnitt: en gransknings-yta (audit surface), inte en export-funktion
Teckensnitts-API:et besvarar (answers) frågor om teckensnitt (fonts). Det räcker dig (hands you) inte själva teckensnittsfilerna, och den åtskillnaden (distinction) formar (shapes) allt du kan bygga ovanpå (on) det. Efter att FindFonts har skannat dokumentet, vandrar uppräkningen (enumeration walks) genom teckensnitten via ID, och egenskaps-anropen (property calls) rapporterar om vilket teckensnitt som för närvarande (currently) är valt (selected):
var
I: Integer;
begin
Pdf.FindFonts;
for I := 1 to Pdf.FontCount do // font indexes start at 1, not 0
if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
Writeln(Format('%s type=%d embedded=%d subset=%d',
[Pdf.FontName, Pdf.FontType,
Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;
Håll ett öga på loop-gränserna (loop bounds). Teckensnitts-index går (run) från 1 till FontCount, medan textblocks- och bildliste-indexen några stycken (paragraphs) upp är noll-baserade (zero-based). Bär (Carry) ena konventionen (convention) in i den andra och du får ett off-by-one-fel som antingen (either) hoppar över (skips) det första teckensnittet eller löper över (runs off) slutet, och det kommer att passera slentrianmässig (casual) testning (testing) eftersom (because) de flesta dokument har åtskilliga (several) teckensnitt och fel sådant fortfarande ser rimligt (plausible) ut. Var tydlig (clear) angående räckvidd (scope), också. Det här API:et har ingen teckensnittsexport (font export) på byte-nivå (byte-level). Inget anrop returnerar (returns) det inbäddade teckensnitts-programmet som en TTF- eller OTF-fil, och uppräkning (enumeration) plus metadata-inspektion är hela den avsedda (intended) modellen. Den modellen täcker (covers) fortfarande (still) det som produktionsarbete faktiskt begär (asks of) av teckensnitt: delmängds-upptäckt (subset detection) genom namnmönster (name pattern), inbäddnings-granskningar (embedding audits) innan en arkiveringskonvertering (archival conversion) (ett icke-inbäddat (unembedded) teckensnitt är en hård PDF/A-blockerare, som PDF/A- och PDF/UA-preflight i Delphi går in på (goes into)), och kodnings-diagnostik (encoding diagnostics) för när extraktions-konfidensen sjunker. Det finns även (also) en licens-orsak till att gränsen sitter (sits) här. Ett delmängds-teckensnittsprogram (subset font program) är licensierat material och, då det saknar (missing) de flesta av sina glyfer, oanvändbart som ett installerbart teckensnitt ändå. Att behandla det som gransknings-metadata snarare än en extraherbar (extractable) tillgång (asset) är ståndpunkten (position) du kan försvara
Det sista anropet drar sitt strå till stacken (pulls its weight) vid triage (triage). Kör GetFontEncoding på varje teckensnitt, läs (read) det vid sidan av (alongside) delmängdsflaggan (subset flag), och du kan förutsäga (predict) extraktionskvalitet innan du drar ett enda tecken. En sida vars teckensnitt alla är delmängder med icke-standardiserade kodningar (encodings) är en OCR-kandidat enbart på inspektion, vilket låter en batch-pipeline dirigera (route) den korrekt utan att (without) först slösa ett misslyckat (failed) extraktionspass på den
Extraktion i skala (at scale) utan att läsa in dokument
I en batch-pipeline är inläsning (loading) av ett helt (entire) dokument bara för att (just to) läsa en sida bortslösad I/O, och det adderas snabbt upp (adds up fast) över (across) en korpus. Enkelt-anropsvarianterna, ExtractFilePageText och ExtractFilePageTextBlocks, tar ett filnamn, lösenord och sidnummer direkt och hoppar över den fulla inläsningen (full load). För filer i gigabyte-skala (gigabyte-scale) finns (there is) en ännu (still) lägre växel (gear). Den direkta åtkomst-vägen (direct-access path) öppnar en fil genom (through) strömmande (streaming) xref-läsningar (xref reads), så att DAOpenFileReadOnly följt av DAExtractPageText rör (touches) endast (only) de objekt (objects) som den enda (one) sidan faktiskt behöver (needs). Det kommer med ett konventions-skifte (convention shift) värt att lägga (committing to) på minnet (memory): DA-funktionerna adresserar (address) sidor genom PageRef, ett objekt-referens-handtag (object-reference handle) du får från (get from) DAFindPage, aldrig genom (never by) rått (raw) sidnummer. Skicka (Pass) in numret där (where) handtaget hör hemma (belongs) och anropet opererar (operates) på fel (wrong) objekt utan att utlösa ett fel (raising an error), vilket är den värsta (worst) typen av (kind of) misstag (mistake) att avlusa (debug). Resten av direktåtkomst-verktygslådan (direct-access toolkit) är (is) utlagd (laid out) i sammanslagning, uppdelning och direktåtkomst av stora PDF-filer
Om (If) det finns en enda vana (habit) som separerar extraktionskod som överlever en riktig korpus från kod som haltar (limps), är det (it is) att behandla sidan som opålitlig indata (untrusted input) snarare än en ren (clean) datakälla. Text som inte stämmer överens (disagrees) med vad visaren renderar är (is) nästan alltid ett kodningsproblem (encoding problem), en ligatur som kollapsar till en (one) glyf eller (or) ett delmängds-teckensnitt (subset font) som saknar sina ToUnicode-poster (ToUnicode entries), och (and) fixen (fix) är (is) att mäta (measure) konfidens och (and) avleda (divert) de dåliga (bad) sidorna till OCR, inte (not) att strida (fight) mot byten (bytes). Teckensnitts-API:et kommer aldrig (never) att (to) producera (produce) en (a) TTF eller OTF, by design, så (so) bygg teckensnitts-arbetsflöden kring granskningsfrågor (audit questions). Och det beständiga extraktionstillståndet, area-rektangeln mest av allt (most of all), är en inställning (setting) du äger under dokumenthandtagets livslängd (for the life of a document handle), inte en parameter du glömmer (forget) efter (after) ett anrop. Få (Get) de där (those) tre (three) reflexerna (reflexes) rätt (right) och (and) resten av (the rest of the) API:et (API) beter sig (behaves)
Utvärderings-byggen (Evaluation builds), demo-projekt (demo projects), och den fullständiga referensen för extraktions-API:et (extraction API reference) finns (are on) på produktsidan för losLab PDF Library för Delphi