Comment puis-je extraire correctement l’indice / indice d’un fichier PDF à l’aide de iTextSharp?

iTextSharp fonctionne bien pour extraire du texte brut à partir de documents PDF, mais je rencontre des problèmes avec le texte en indice / exposant, courant dans les documents techniques.

TextChunk.SameLine() nécessite que deux morceaux aient un positionnement vertical identique pour être “sur” la même ligne, ce qui n’est pas le cas pour le texte en exposant ou en indice. Par exemple, à la page 11 de ce document, sous “EFFICACITÉ DE LA COMBUSTION”:

http://www.mass.gov/courts/docs/lawlib/300-399cmr/310cmr7.pdf

Texte attendu:

 monoxide (CO) in flue gas in accordance with the following formula: CE = [CO2 /(CO + CO2)] 

Texte de résultat:

 monoxide (CO) in flue gas in accordance with the following formula: CE = [CO /(CO + CO )] 2 2 

J’ai déplacé SameLine() vers LocationTextExtractionStrategy et rendu publics des getters pour les propriétés privées TextChunk . Cela m’a permis d’ajuster la tolérance à la volée dans ma propre sous-classe, illustrée ici:

 public class SubSuperStrategy : LocationTextExtractionStrategy { public int SameLineOrientationTolerance { get; set; } public int SameLineDistanceTolerance { get; set; } public override bool SameLine(TextChunk chunk1, TextChunk chunk2) { var orientationDelta = Math.Abs(chunk1.OrientationMagnitude - chunk2.OrientationMagnitude); if(orientationDelta > SameLineOrientationTolerance) return false; var distDelta = Math.Abs(chunk1.DistPerpendicular - chunk2.DistPerpendicular); return (distDelta <= SameLineDistanceTolerance); } } 

L’utilisation de SameLineDistanceTolerance de 3 corrige la ligne à laquelle les sous / super morceaux sont affectés, mais la position relative du texte est très différente:

 monoxide (CO) in flue gas in accordance with the following formula: CE = [CO /(CO + CO )] 2 2 

Parfois, les morceaux sont insérés quelque part au milieu du texte et parfois (comme dans cet exemple) à la fin. De toute façon, ils ne se retrouvent pas au bon endroit. Je soupçonne que cela pourrait avoir quelque chose à voir avec les tailles de police, mais je suis à la limite de ma compréhension des entrailles de ce code.

Quelqu’un a-t-il trouvé un autre moyen de gérer cela?

(Je suis heureux de soumettre une demande de retrait avec mes modifications si cela peut vous aider.)

Pour extraire correctement ces indices et exposants en ligne, il faut adopter une approche différente pour vérifier si deux fragments de texte se trouvent sur la même ligne. Les classes suivantes représentent une telle approche.

Je suis plus à l’aise en Java / iText; J’ai donc d’abord implémenté cette approche en Java, puis je l’ai traduite en C # / iTextSharp.

Une approche utilisant Java et iText

J’utilise l’actuelle twig de développement iText 5.5.8-SNAPSHOT.

Un moyen d’identifier les lignes

En supposant que les lignes de texte soient horizontales et que les bordures des glyphes s’étendent verticalement sur différentes lignes sans se chevaucher, on peut essayer d’identifier les lignes à l’aide d’un RenderListener comme ceci:

 public class TextLineFinder implements RenderListener { @Override public void beginTextBlock() { } @Override public void endTextBlock() { } @Override public void renderImage(ImageRenderInfo renderInfo) { } /* * @see RenderListener#renderText(TextRenderInfo) */ @Override public void renderText(TextRenderInfo renderInfo) { LineSegment ascentLine = renderInfo.getAscentLine(); LineSegment descentLine = renderInfo.getDescentLine(); float[] yCoords = new float[]{ ascentLine.getStartPoint().get(Vector.I2), ascentLine.getEndPoint().get(Vector.I2), descentLine.getStartPoint().get(Vector.I2), descentLine.getEndPoint().get(Vector.I2) }; Arrays.sort(yCoords); addVerticalUseSection(yCoords[0], yCoords[3]); } /** * This method marks the given interval as used. */ void addVerticalUseSection(float from, float to) { if (to < from) { float temp = to; to = from; from = temp; } int i=0, j=0; for (; i i) verticalFlips.remove(j); if (toOutsideInterval) verticalFlips.add(i, to); if (fromOutsideInterval) verticalFlips.add(i, from); } final List verticalFlips = new ArrayList(); } 

( TextLineFinder.java )

RenderListener tente d’identifier les lignes de texte horizontales en projetant les zones de délimitation du texte sur l’axe des ordonnées. Cela suppose que ces projections ne se chevauchent pas pour du texte provenant de lignes différentes, même dans le cas des indices et des indices supérieurs.

Cette classe est essentiellement une forme réduite du PageVerticalAnalyzer utilisé dans cette réponse .

Tri des morceaux de texte selon ces lignes

Après avoir identifié les lignes comme ci-dessus, on peut modifier LocationTextExtractionStrategy d’iText pour sortinger les lignes comme suit:

 public class HorizontalTextExtractionStrategy extends LocationTextExtractionStrategy { public class HorizontalTextChunk extends TextChunk { public HorizontalTextChunk(Ssortingng ssortingng, Vector startLocation, Vector endLocation, float charSpaceWidth) { super(ssortingng, startLocation, endLocation, charSpaceWidth); } @Override public int compareTo(TextChunk rhs) { if (rhs instanceof HorizontalTextChunk) { HorizontalTextChunk horRhs = (HorizontalTextChunk) rhs; int rslt = Integer.compare(getLineNumber(), horRhs.getLineNumber()); if (rslt != 0) return rslt; return Float.compare(getStartLocation().get(Vector.I1), rhs.getStartLocation().get(Vector.I1)); } else return super.compareTo(rhs); } @Override public boolean sameLine(TextChunk as) { if (as instanceof HorizontalTextChunk) { HorizontalTextChunk horAs = (HorizontalTextChunk) as; return getLineNumber() == horAs.getLineNumber(); } else return super.sameLine(as); } public int getLineNumber() { Vector startLocation = getStartLocation(); float y = startLocation.get(Vector.I2); List flips = textLineFinder.verticalFlips; if (flips == null || flips.isEmpty()) return 0; if (y < flips.get(0)) return flips.size() / 2 + 1; for (int i = 1; i < flips.size(); i+=2) { if (y < flips.get(i)) { return (1 + flips.size() - i) / 2; } } return 0; } } @Override public void renderText(TextRenderInfo renderInfo) { textLineFinder.renderText(renderInfo); LineSegment segment = renderInfo.getBaseline(); if (renderInfo.getRise() != 0){ // remove the rise from the baseline - we do this because the text from a super/subscript render operations should probably be considered as part of the baseline of the text the super/sub is relative to Matrix riseOffsetTransform = new Matrix(0, -renderInfo.getRise()); segment = segment.transformBy(riseOffsetTransform); } TextChunk location = new HorizontalTextChunk(renderInfo.getText(), segment.getStartPoint(), segment.getEndPoint(), renderInfo.getSingleSpaceWidth()); getLocationalResult().add(location); } public HorizontalTextExtractionStrategy() throws NoSuchFieldException, SecurityException { locationalResultField = LocationTextExtractionStrategy.class.getDeclaredField("locationalResult"); locationalResultField.setAccessible(true); textLineFinder = new TextLineFinder(); } @SuppressWarnings("unchecked") List getLocationalResult() { try { return (List) locationalResultField.get(this); } catch (IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); throw new RuntimeException(e); } } final Field locationalResultField; final TextLineFinder textLineFinder; } 

( HorizontalTextExtractionStrategy.java )

Cette TextExtractionStrategy utilise un TextLineFinder pour identifier les lignes de texte horizontales, puis utilise ces informations pour sortinger les fragments de texte.

Attention, ce code utilise la reflection pour accéder aux membres de la classe parent privée. Cela peut ne pas être autorisé dans tous les environnements. Dans ce cas, copiez simplement la LocationTextExtractionStrategy et insérez directement le code.

Extraire le texte

Maintenant, on peut utiliser cette stratégie d’extraction de texte pour extraire le texte avec des exposants et des indices intégrés comme ceci:

 Ssortingng extract(PdfReader reader, int pageNo) throws IOException, NoSuchFieldException, SecurityException { return PdfTextExtractor.getTextFromPage(reader, pageNo, new HorizontalTextExtractionStrategy()); } 

(de ExtractSuperAndSubInLine.java )

L’exemple de texte de la page 11 du document du PO, sous “EFFICACITÉ DE LA COMBUSTION”, est maintenant extrait comme suit:

 monoxide (CO) in flue gas in accordance with the following formula: CE = [CO 2/(CO + CO 2 )] 

La même approche utilisant C # & iTextSharp

Les explications, les avertissements et les exemples de résultats de la section centrée sur Java s’appliquent toujours. Voici le code:

J’utilise iTextSharp 5.5.7.

Un moyen d’identifier les lignes

 public class TextLineFinder : IRenderListener { public void BeginTextBlock() { } public void EndTextBlock() { } public void RenderImage(ImageRenderInfo renderInfo) { } public void RenderText(TextRenderInfo renderInfo) { LineSegment ascentLine = renderInfo.GetAscentLine(); LineSegment descentLine = renderInfo.GetDescentLine(); float[] yCoords = new float[]{ ascentLine.GetStartPoint()[Vector.I2], ascentLine.GetEndPoint()[Vector.I2], descentLine.GetStartPoint()[Vector.I2], descentLine.GetEndPoint()[Vector.I2] }; Array.Sort(yCoords); addVerticalUseSection(yCoords[0], yCoords[3]); } void addVerticalUseSection(float from, float to) { if (to < from) { float temp = to; to = from; from = temp; } int i=0, j=0; for (; i i) verticalFlips.RemoveAt(j); if (toOutsideInterval) verticalFlips.Insert(i, to); if (fromOutsideInterval) verticalFlips.Insert(i, from); } public List verticalFlips = new List(); } 

Tri des morceaux de texte selon ces lignes

 public class HorizontalTextExtractionStrategy : LocationTextExtractionStrategy { public class HorizontalTextChunk : TextChunk { public HorizontalTextChunk(Ssortingng ssortingngValue, Vector startLocation, Vector endLocation, float charSpaceWidth, TextLineFinder textLineFinder) : base(ssortingngValue, startLocation, endLocation, charSpaceWidth) { this.textLineFinder = textLineFinder; } override public int CompareTo(TextChunk rhs) { if (rhs is HorizontalTextChunk) { HorizontalTextChunk horRhs = (HorizontalTextChunk) rhs; int rslt = CompareInts(getLineNumber(), horRhs.getLineNumber()); if (rslt != 0) return rslt; return CompareFloats(StartLocation[Vector.I1], rhs.StartLocation[Vector.I1]); } else return base.CompareTo(rhs); } public override bool SameLine(TextChunk a) { if (a is HorizontalTextChunk) { HorizontalTextChunk horAs = (HorizontalTextChunk) a; return getLineNumber() == horAs.getLineNumber(); } else return base.SameLine(a); } public int getLineNumber() { Vector startLocation = StartLocation; float y = startLocation[Vector.I2]; List flips = textLineFinder.verticalFlips; if (flips == null || flips.Count == 0) return 0; if (y < flips[0]) return flips.Count / 2 + 1; for (int i = 1; i < flips.Count; i+=2) { if (y < flips[i]) { return (1 + flips.Count - i) / 2; } } return 0; } private static int CompareInts(int int1, int int2){ return int1 == int2 ? 0 : int1 < int2 ? -1 : 1; } private static int CompareFloats(float float1, float float2) { return float1 == float2 ? 0 : float1 < float2 ? -1 : 1; } TextLineFinder textLineFinder; } public override void RenderText(TextRenderInfo renderInfo) { textLineFinder.RenderText(renderInfo); LineSegment segment = renderInfo.GetBaseline(); if (renderInfo.GetRise() != 0){ // remove the rise from the baseline - we do this because the text from a super/subscript render operations should probably be considered as part of the baseline of the text the super/sub is relative to Matrix riseOffsetTransform = new Matrix(0, -renderInfo.GetRise()); segment = segment.TransformBy(riseOffsetTransform); } TextChunk location = new HorizontalTextChunk(renderInfo.GetText(), segment.GetStartPoint(), segment.GetEndPoint(), renderInfo.GetSingleSpaceWidth(), textLineFinder); getLocationalResult().Add(location); } public HorizontalTextExtractionStrategy() { locationalResultField = typeof(LocationTextExtractionStrategy).GetField("locationalResult", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); textLineFinder = new TextLineFinder(); } List getLocationalResult() { return (List) locationalResultField.GetValue(this); } System.Reflection.FieldInfo locationalResultField; TextLineFinder textLineFinder; } 

Extraire le texte

  ssortingng extract(PdfReader reader, int pageNo) { return PdfTextExtractor.GetTextFromPage(reader, pageNo, new HorizontalTextExtractionStrategy()); } 

UPDATE: Modifications de LocationTextExtractionStrategy

La taille de l’article est 5.5.9-SNAPSHOT Commits. reflection.

Malheureusement, cette modification rompt la HorizontalTextExtractionStrategy présentée ci-dessus. Pour les versions iText après ces validations, on peut utiliser la stratégie suivante:

 public class HorizontalTextExtractionStrategy2 extends LocationTextExtractionStrategy { public static class HorizontalTextChunkLocationStrategy implements TextChunkLocationStrategy { public HorizontalTextChunkLocationStrategy(TextLineFinder textLineFinder) { this.textLineFinder = textLineFinder; } @Override public TextChunkLocation createLocation(TextRenderInfo renderInfo, LineSegment baseline) { return new HorizontalTextChunkLocation(baseline.getStartPoint(), baseline.getEndPoint(), renderInfo.getSingleSpaceWidth()); } final TextLineFinder textLineFinder; public class HorizontalTextChunkLocation implements TextChunkLocation { /** the starting location of the chunk */ private final Vector startLocation; /** the ending location of the chunk */ private final Vector endLocation; /** unit vector in the orientation of the chunk */ private final Vector orientationVector; /** the orientation as a scalar for quick sorting */ private final int orientationMagnitude; /** perpendicular distance to the orientation unit vector (ie the Y position in an unrotated coordinate system) * we round to the nearest integer to handle the fuzziness of comparing floats */ private final int distPerpendicular; /** distance of the start of the chunk parallel to the orientation unit vector (ie the X position in an unrotated coordinate system) */ private final float distParallelStart; /** distance of the end of the chunk parallel to the orientation unit vector (ie the X position in an unrotated coordinate system) */ private final float distParallelEnd; /** the width of a single space character in the font of the chunk */ private final float charSpaceWidth; public HorizontalTextChunkLocation(Vector startLocation, Vector endLocation, float charSpaceWidth) { this.startLocation = startLocation; this.endLocation = endLocation; this.charSpaceWidth = charSpaceWidth; Vector oVector = endLocation.subtract(startLocation); if (oVector.length() == 0) { oVector = new Vector(1, 0, 0); } orientationVector = oVector.normalize(); orientationMagnitude = (int)(Math.atan2(orientationVector.get(Vector.I2), orientationVector.get(Vector.I1))*1000); // see http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html // the two vectors we are crossing are in the same plane, so the result will be purely // in the z-axis (out of plane) direction, so we just take the I3 component of the result Vector origin = new Vector(0,0,1); distPerpendicular = (int)(startLocation.subtract(origin)).cross(orientationVector).get(Vector.I3); distParallelStart = orientationVector.dot(startLocation); distParallelEnd = orientationVector.dot(endLocation); } public int orientationMagnitude() { return orientationMagnitude; } public int distPerpendicular() { return distPerpendicular; } public float distParallelStart() { return distParallelStart; } public float distParallelEnd() { return distParallelEnd; } public Vector getStartLocation() { return startLocation; } public Vector getEndLocation() { return endLocation; } public float getCharSpaceWidth() { return charSpaceWidth; } /** * @param as the location to compare to * @return true is this location is on the the same line as the other */ public boolean sameLine(TextChunkLocation as) { if (as instanceof HorizontalTextChunkLocation) { HorizontalTextChunkLocation horAs = (HorizontalTextChunkLocation) as; return getLineNumber() == horAs.getLineNumber(); } else return orientationMagnitude() == as.orientationMagnitude() && distPerpendicular() == as.distPerpendicular(); } /** * Computes the distance between the end of 'other' and the beginning of this chunk * in the direction of this chunk's orientation vector. Note that it's a bad idea * to call this for chunks that aren't on the same line and orientation, but we don't * explicitly check for that condition for performance reasons. * @param other * @return the number of spaces between the end of 'other' and the beginning of this chunk */ public float distanceFromEndOf(TextChunkLocation other) { float distance = distParallelStart() - other.distParallelEnd(); return distance; } public boolean isAtWordBoundary(TextChunkLocation previous) { /** * Here we handle a very specific case which in PDF may look like: * -.232 Tc [( P)-226.2(r)-231.8(e)-230.8(f)-238(a)-238.9(c)-228.9(e)]TJ * The font's charSpace width is 0.232 and it's compensated with charSpacing of 0.232. * And a resultant TextChunk.charSpaceWidth comes to TextChunk constructor as 0. * In this case every chunk is considered as a word boundary and space is added. * We should consider charSpaceWidth equal (or close) to zero as a no-space. */ if (getCharSpaceWidth() < 0.1f) return false; float dist = distanceFromEndOf(previous); return dist < -getCharSpaceWidth() || dist > getCharSpaceWidth()/2.0f; } public int getLineNumber() { Vector startLocation = getStartLocation(); float y = startLocation.get(Vector.I2); List flips = textLineFinder.verticalFlips; if (flips == null || flips.isEmpty()) return 0; if (y < flips.get(0)) return flips.size() / 2 + 1; for (int i = 1; i < flips.size(); i+=2) { if (y < flips.get(i)) { return (1 + flips.size() - i) / 2; } } return 0; } @Override public int compareTo(TextChunkLocation rhs) { if (rhs instanceof HorizontalTextChunkLocation) { HorizontalTextChunkLocation horRhs = (HorizontalTextChunkLocation) rhs; int rslt = Integer.compare(getLineNumber(), horRhs.getLineNumber()); if (rslt != 0) return rslt; return Float.compare(getStartLocation().get(Vector.I1), rhs.getStartLocation().get(Vector.I1)); } else { int rslt; rslt = Integer.compare(orientationMagnitude(), rhs.orientationMagnitude()); if (rslt != 0) return rslt; rslt = Integer.compare(distPerpendicular(), rhs.distPerpendicular()); if (rslt != 0) return rslt; return Float.compare(distParallelStart(), rhs.distParallelStart()); } } } } @Override public void renderText(TextRenderInfo renderInfo) { textLineFinder.renderText(renderInfo); super.renderText(renderInfo); } public HorizontalTextExtractionStrategy2() throws NoSuchFieldException, SecurityException { this(new TextLineFinder()); } public HorizontalTextExtractionStrategy2(TextLineFinder textLineFinder) throws NoSuchFieldException, SecurityException { super(new HorizontalTextChunkLocationStrategy(textLineFinder)); this.textLineFinder = textLineFinder; } final TextLineFinder textLineFinder; } 

( HorizontalTextExtractionStrategy2.java )

Je viens de résoudre un problème similaire, voir ma question . Je détecte les indices en tant que texte ayant une ligne de base entre les lignes croissante et décroissante du texte précédent. Ce bout de code pourrait être utile:

  Vector thisFacade = this.ascentLine.GetStartPoint().Subtract(this.descentLine.GetStartPoint()); Vector infoFacade = renderInfo.GetAscentLine().GetStartPoint().Subtract(renderInfo.GetDescentLine().GetStartPoint()); if (baseVector.Cross(ascent2base).Dot(baseVector.Cross(descent2base)) < 0 && infoFacade.LengthSquared < thisFacade.LengthSquared - sameHeightThreshols) 

Plus de détails après Chistmass.