2014-12-30 10 views
12

Ho un semplice file pdf, contenente le parole "Ciao mondo", ognuna di un colore diverso.pdf.js: Ottieni il colore del testo

Sto caricando il PDF, in questo modo:

PDFJS.getDocument('test.pdf').then(onPDF); 

function onPDF(pdf) 
{ 
    pdf.getPage(1).then(onPage); 
} 

function onPage(page) 
{ 
    page.getTextContent().then(onText); 
} 

function onText(text) 
{ 
    console.log(JSON.stringify(text)); 
} 

E ho un uscita JSON come questo:

{ 
    "items" : [{ 
      "str" : "Hello ", 
      "dir" : "ltr", 
      "width" : 29.592, 
      "height" : 12, 
      "transform" : [12, 0, 0, 12, 56.8, 774.1], 
      "fontName" : "g_font_1" 
     }, { 
      "str" : "world", 
      "dir" : "ltr", 
      "width" : 27.983999999999998, 
      "height" : 12, 
      "transform" : [12, 0, 0, 12, 86.5, 774.1], 
      "fontName" : "g_font_1" 
     } 
    ], 
    "styles" : { 
     "g_font_1" : { 
      "fontFamily" : "serif", 
      "ascent" : 0.891, 
      "descent" : 0.216 
     } 
    } 
} 

Tuttavia, non sono stato in grado di trovare un modo per determinare il colore di ogni parola. Quando lo renderò, lo renderò correttamente, quindi so che l'informazione è lì dentro da qualche parte. C'è un posto dove posso accedervi?

+1

Non che semplice, le specifiche PDF definisce due percorsi di codice per la pittura e l'estrazione di testo. PDF.js sta estraendo del testo su https://github.com/mozilla/pdf.js/blob/master/src/core/evaluator.js#L886 e crea comandi di pittura in posizioni diverse. Puoi estendere il precedente con le informazioni sul colore se necessario (potrebbe non essere solo un colore) – async5

+0

Hai un'idea di dove recupera i dati dei colori? Potrei riuscire a modificare qualcosa per quello che mi serve – divillysausages

+1

Vedere http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/PDF32000_2008.pdf per diversi operatori che controllano lo stile di riempimento e di tracciamento , che include gradienti, motivi e assegnazione tramite spazio colore. Esempio di tale trattamento su https://github.com/mozilla/pdf.js/blob/master/src/core/evaluator.js#L758 – async5

risposta

6

Come Rispawned alluso a, non c'è una risposta facile che funzionerà in tutti i casi. Detto questo, ecco due approcci che sembrano funzionare abbastanza bene. Entrambi con lati positivi e negativi.

Approccio 1

Internamente, il metodo utilizza getTextContent che cosa è chiamato un EvaluatorPreprocessor per analizzare gli operatori PDF, e mantenere lo stato grafico.Quindi, quello che possiamo fare è, implementare un custom EvaluatorPreprocessor, sovrascrivere il metodo preprocessCommand e utilizzarlo per aggiungere il colore del testo corrente allo stato grafico. Una volta che questo è a posto, ogni volta che viene creato un nuovo blocco di testo, possiamo aggiungere un attributo colore e impostarlo sullo stato corrente del colore.

Gli svantaggi di questo approccio sono:

  1. Richiede la modifica del codice sorgente PDFJS. Dipende anche pesantemente da l'attuale implementazione di PDFJS, e potrebbe interrompersi se questo è modificato.

  2. Fallirà nei casi in cui il testo viene utilizzato come percorso da riempire con un'immagine. In alcuni creatori di PDF (come Photoshop), il modo in cui crea il testo colorato, crea dapprima un tracciato di ritaglio da tutti i caratteri di testo, e quindi dipinge un'immagine solida sul percorso. Quindi l'unico modo per dedurre il colore di riempimento è leggere i valori dei pixel dall'immagine, che richiederebbero di dipingerlo su una tela. Anche l'aggancio in paintChar non sarà di grande aiuto qui, poiché il colore di riempimento emergerà solo in un secondo momento.

Il vantaggio è, la sua abbastanza robusto e lavora indipendentemente lo sfondo della pagina. Inoltre non richiede il rendering di nulla su tela, quindi può essere fatto interamente nel thread in background.

Codice

Tutte le modifiche sono fatte nel file core/evaluator.js.

Per prima cosa è necessario definire il valutatore personalizzato, dopo lo EvaluatorPreprocessor definition.

var CustomEvaluatorPreprocessor = (function() { 
    function CustomEvaluatorPreprocessor(stream, xref, stateManager, resources) { 
     EvaluatorPreprocessor.call(this, stream, xref, stateManager); 
     this.resources = resources; 
     this.xref = xref; 

     // set initial color state 
     var state = this.stateManager.state; 
     state.textRenderingMode = TextRenderingMode.FILL; 
     state.fillColorSpace = ColorSpace.singletons.gray; 
     state.fillColor = [0,0,0]; 
    } 

    CustomEvaluatorPreprocessor.prototype = Object.create(EvaluatorPreprocessor.prototype); 

    CustomEvaluatorPreprocessor.prototype.preprocessCommand = function(fn, args) { 
     EvaluatorPreprocessor.prototype.preprocessCommand.call(this, fn, args); 
     var state = this.stateManager.state; 
     switch(fn) { 
      case OPS.setFillColorSpace: 
       state.fillColorSpace = ColorSpace.parse(args[0], this.xref, this.resources); 
      break; 
      case OPS.setFillColor: 
       var cs = state.fillColorSpace; 
       state.fillColor = cs.getRgb(args, 0); 
      break; 
      case OPS.setFillGray: 
       state.fillColorSpace = ColorSpace.singletons.gray; 
       state.fillColor = ColorSpace.singletons.gray.getRgb(args, 0); 
      break; 
      case OPS.setFillCMYKColor: 
       state.fillColorSpace = ColorSpace.singletons.cmyk; 
       state.fillColor = ColorSpace.singletons.cmyk.getRgb(args, 0); 
      break; 
      case OPS.setFillRGBColor: 
       state.fillColorSpace = ColorSpace.singletons.rgb; 
       state.fillColor = ColorSpace.singletons.rgb.getRgb(args, 0); 
      break; 
     } 
    }; 

    return CustomEvaluatorPreprocessor; 
})(); 

Successivamente, è necessario modificare il getTextContent method di utilizzare la nuova valutatore:

var preprocessor = new CustomEvaluatorPreprocessor(stream, xref, stateManager, resources); 

E, infine, nel metodo newTextChunk, aggiungere un attributo color:

color: stateManager.state.fillColor 

Approccio 2

Un altro approccio sarebbe quello di estrarre la casella di delimitazione del testo s tramite getTextContent, esegue il rendering della pagina e, per ciascun testo, ottiene i valori dei pixel che si trovano all'interno dei suoi limiti e assume che sia il colore di riempimento.

Gli svantaggi di questo approccio sono:

  1. I calcolati caselle testo delimitazione non sono sempre corretto, e in alcuni casi possono anche essere spento completamente (es: testo ruotato). Se il riquadro di delimitazione non copre almeno parzialmente il testo reale su tela, allora questo metodo fallirà. Siamo in grado di recuperare da guasti completi, controllando che i pixel del testo abbiano una varianza di colore maggiore di una soglia. Essendo la base logica, se il riquadro di delimitazione è completamente sullo sfondo, avrà poca varianza, nel qual caso possiamo ricorrere a un colore di testo predefinito (o forse anche al colore di k vicini più vicini).
  2. Il metodo presuppone che il testo sia più scuro dello sfondo. In caso contrario, lo sfondo potrebbe essere confuso con il colore di riempimento. Questo non costituirà un problema nella maggior parte dei casi, poiché la maggior parte dei documenti ha sfondi bianchi.

Il vantaggio è, è semplice, e non richiede problemi con il codice sorgente PDFJS. Inoltre, funzionerà nei casi in cui il testo viene utilizzato come tracciato di ritaglio e riempito con un'immagine. Anche se questo può diventare confuso quando si hanno riempimenti di immagine complessi, nel qual caso la scelta del colore del testo diventa ambigua.

Demo

http://jsfiddle.net/x2rajt5g/

campione PDF di prova:

Codice

function parseColors(canvasImgData, texts) { 
    var data = canvasImgData.data, 
     width = canvasImgData.width, 
     height = canvasImgData.height, 
     defaultColor = [0, 0, 0], 
     minVariance = 20; 

    texts.forEach(function (t) { 
     var left = Math.floor(t.transform[4]), 
      w = Math.round(t.width), 
      h = Math.round(t.height), 
      bottom = Math.round(height - t.transform[5]), 
      top = bottom - h, 
      start = (left + (top * width)) * 4, 
      color = [], 
      best = Infinity, 
      stat = new ImageStats(); 

     for (var i, v, row = 0; row < h; row++) { 
      i = start + (row * width * 4); 
      for (var col = 0; col < w; col++) { 
       if ((v = data[i] + data[i + 1] + data[i + 2]) < best) { // the darker the "better" 
        best = v; 
        color[0] = data[i]; 
        color[1] = data[i + 1]; 
        color[2] = data[i + 2]; 
       } 
       stat.addPixel(data[i], data[i+1], data[i+2]); 
       i += 4; 
      } 
     } 
     var stdDev = stat.getStdDev(); 
     t.color = stdDev < minVariance ? defaultColor : color; 
    }); 
} 

function ImageStats() { 
    this.pixelCount = 0; 
    this.pixels = []; 
    this.rgb = []; 
    this.mean = 0; 
    this.stdDev = 0; 
} 

ImageStats.prototype = { 
    addPixel: function (r, g, b) { 
     if (!this.rgb.length) { 
      this.rgb[0] = r; 
      this.rgb[1] = g; 
      this.rgb[2] = b; 
     } else { 
      this.rgb[0] += r; 
      this.rgb[1] += g; 
      this.rgb[2] += b; 
     } 
     this.pixelCount++; 
     this.pixels.push([r,g,b]); 
    }, 

    getStdDev: function() { 
     var mean = [ 
      this.rgb[0]/this.pixelCount, 
      this.rgb[1]/this.pixelCount, 
      this.rgb[2]/this.pixelCount 
     ]; 
     var diff = [0,0,0]; 
     this.pixels.forEach(function(p) { 
      diff[0] += Math.pow(mean[0] - p[0], 2); 
      diff[1] += Math.pow(mean[1] - p[1], 2); 
      diff[2] += Math.pow(mean[2] - p[2], 2); 
     }); 
     diff[0] = Math.sqrt(diff[0]/this.pixelCount); 
     diff[1] = Math.sqrt(diff[1]/this.pixelCount); 
     diff[2] = Math.sqrt(diff[2]/this.pixelCount); 
     return diff[0] + diff[1] + diff[2]; 
    } 
}; 
+0

Holy hell, questo è un lavoro straordinario; mi fa venir voglia di essere revocato più volte. Testare rapidamente il secondo approccio sta funzionando bene finora, grazie! – divillysausages

+0

Questo sicuramente ottiene il mio voto per essere più dettagliato. Vorrei tuttavia evitare di sovrascrivere il secondo metodo come sempre (o spesso) di lavoro per tracciati di ritaglio perché potrebbe esserci qualcosa al di sotto, [ad es. un'immagine arbitraria] (http://www.pdflib.com/fileadmin/pdflib/Cookbook/pdf/text_as_clipping_path.pdf), per cui la nozione di (un singolo) colore di riempimento non è particolarmente ben definita. Non vedo perché qualcuno (o qualche programma) generi percorsi di ritaglio del testo su un colore a tinta unita quando è più economico utilizzare il riempimento. Un tracciato di ritaglio viene in genere utilizzato in casi difficili come quello che ho collegato. – Fizz

+0

Il PDF creato da Photoshop contiene [questa immagine] (http://imgur.com/676lg6i) dipinta sopra il tracciato di ritaglio del testo. Un PDF simile (con lo stesso testo e con lo stesso font incorporato) prodotto in Inkscape ha solo 12 KB rispetto agli 86 KB di Photoshop, quindi non sembra intelligente in termini di dimensioni. Photoshop disegna inoltre i caratteri uno per uno (Tj), non con i comandi di matrice (TJ) e esegue anche una trasformazione di spostamento (Tm) per ogni lettera anziché ogni riga. Quindi scrivere un sacco di testo PDF in Photoshop non sembra una buona idea.Inkscape esegue la normale procedura di riempimento con i colori dei caratteri (senza immagini). – Fizz

5

Questa domanda è in realtà estremamente difficile se si vuole farlo alla perfezione ... o può essere relativamente facile se si può vivere con soluzioni che funzionano solo una parte del tempo.

Prima di tutto, rendi conto che lo getTextContent è destinato all'estrazione di testo ricercabile ed è tutto ciò che è destinato a fare.

E 'stato suggerito nei commenti di cui sopra che si utilizza page.getOperatorList(), ma che è fondamentalmente ri-attuazione l'intero modello di disegno PDF nel codice ... che è fondamentalmente stupido perché il più grande pezzo di PDFJS fa esattamente questo ... tranne non a scopo di estrazione del testo ma a scopo di rendering su tela. Quindi, quello che vuoi fare è hackerare canvas.js in modo che invece di impostare solo le sue manopole interne, faccia anche alcune callback al tuo codice. Ahimè, se andate in questo modo, non sarete in grado di usare PDFJS di serie, e dubito piuttosto che il vostro obiettivo di estrazione del colore sarà visto come molto utile per lo scopo principale di PDFJS, quindi probabilmente le vostre modifiche non arriveranno accettato a monte, quindi probabilmente dovrai mantenere il tuo fork di PDFJS.

Dopo questo terribile avvertimento, ciò che è necessario modificare in minima parte sono le funzioni in cui PDFJS ha analizzato gli operatori di colore PDF e imposta il proprio colore di pittura su tela. Ciò accade attorno alla riga 1566 (di canvas.js) in function setFillColorN. Dovrai anche agganciare il rendering del testo ... che è piuttosto un riproduttore di caratteri a livello canvas.js, ovvero CanvasGraphics_paintChar attorno alla riga 1270. Con questi due hooked, avrai un flusso di callback per i cambi di colore inframmezzati tra i caratteri disegnare sequenze. In questo modo è possibile ricostruire il colore delle sequenze di caratteri ragionevolmente semplice da questo .. nei casi di colore semplici.

E ora arrivo alla parte davvero brutta: il fatto che il PDF abbia un modello cromatico estremamente complesso. Per prima cosa ci sono due colori per disegnare qualsiasi cosa, incluso il testo: un colore di riempimento e un colore (contorno) del tratto. Finora non è troppo spaventoso, ma il colore è un indice in un ColorSpace ... di cui ce ne sono diversi, essendo RGB una sola possibilità. Poi ci sono anche le modalità alfa e compositing, quindi i livelli (di vari alpha) possono risultare in un diverso colore finale a seconda della modalità di compositing. E il PDFJS non ha un singolo posto in cui accumula colore da strati .. semplicemente [sopra] li dipinge come vengono. Quindi se estrai solo il colore di riempimento cambia e ignori alpha, compositing ecc. Funzionerà ma non per documenti complessi.

Spero che questo aiuti.

+1

Grazie per quello; Immagino che la mia conoscenza del rendering in PDF avrà bisogno di una grande spinta: D. Vedrò l'aggancio in '_paintChar()' - è per un progetto veloce, quindi non ha bisogno di essere particolarmente ordinato o manutenibile – divillysausages