github.com/unidoc/unidoc@v2.2.0+incompatible/pdf/annotator/line.go (about)

     1  /*
     2   * This file is subject to the terms and conditions defined in
     3   * file 'LICENSE.md', which is part of this source code package.
     4   */
     5  
     6  package annotator
     7  
     8  import (
     9  	"github.com/unidoc/unidoc/common"
    10  	"github.com/unidoc/unidoc/pdf/contentstream/draw"
    11  	pdfcore "github.com/unidoc/unidoc/pdf/core"
    12  	pdf "github.com/unidoc/unidoc/pdf/model"
    13  )
    14  
    15  // Defines a line between point 1 (X1,Y1) and point 2 (X2,Y2).  The line ending styles can be none (regular line),
    16  // or arrows at either end.  The line also has a specified width, color and opacity.
    17  type LineAnnotationDef struct {
    18  	X1               float64
    19  	Y1               float64
    20  	X2               float64
    21  	Y2               float64
    22  	LineColor        *pdf.PdfColorDeviceRGB
    23  	Opacity          float64 // Alpha value (0-1).
    24  	LineWidth        float64
    25  	LineEndingStyle1 draw.LineEndingStyle // Line ending style of point 1.
    26  	LineEndingStyle2 draw.LineEndingStyle // Line ending style of point 2.
    27  }
    28  
    29  // Creates a line annotation object that can be added to page PDF annotations.
    30  func CreateLineAnnotation(lineDef LineAnnotationDef) (*pdf.PdfAnnotation, error) {
    31  	// Line annotation.
    32  	lineAnnotation := pdf.NewPdfAnnotationLine()
    33  
    34  	// Line endpoint locations.
    35  	lineAnnotation.L = pdfcore.MakeArrayFromFloats([]float64{lineDef.X1, lineDef.Y1, lineDef.X2, lineDef.Y2})
    36  
    37  	// Line endings.
    38  	le1 := pdfcore.MakeName("None")
    39  	if lineDef.LineEndingStyle1 == draw.LineEndingStyleArrow {
    40  		le1 = pdfcore.MakeName("ClosedArrow")
    41  	}
    42  	le2 := pdfcore.MakeName("None")
    43  	if lineDef.LineEndingStyle2 == draw.LineEndingStyleArrow {
    44  		le2 = pdfcore.MakeName("ClosedArrow")
    45  	}
    46  	lineAnnotation.LE = pdfcore.MakeArray(le1, le2)
    47  
    48  	// Opacity.
    49  	if lineDef.Opacity < 1.0 {
    50  		lineAnnotation.CA = pdfcore.MakeFloat(lineDef.Opacity)
    51  	}
    52  
    53  	r, g, b := lineDef.LineColor.R(), lineDef.LineColor.G(), lineDef.LineColor.B()
    54  	lineAnnotation.IC = pdfcore.MakeArrayFromFloats([]float64{r, g, b}) // fill color of line endings, rgb 0-1.
    55  	lineAnnotation.C = pdfcore.MakeArrayFromFloats([]float64{r, g, b})  // line color, rgb 0-1.
    56  	bs := pdf.NewBorderStyle()
    57  	bs.SetBorderWidth(lineDef.LineWidth) // Line width: 3 points.
    58  	lineAnnotation.BS = bs.ToPdfObject()
    59  
    60  	// Make the appearance stream (for uniform appearance).
    61  	apDict, bbox, err := makeLineAnnotationAppearanceStream(lineDef)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	lineAnnotation.AP = apDict
    66  
    67  	// The rect specifies the location and dimensions of the annotation.  Technically if the annotation could not
    68  	// be displayed if it goes outside these bounds, although rarely enforced.
    69  	lineAnnotation.Rect = pdfcore.MakeArrayFromFloats([]float64{bbox.Llx, bbox.Lly, bbox.Urx, bbox.Ury})
    70  
    71  	return lineAnnotation.PdfAnnotation, nil
    72  }
    73  
    74  func makeLineAnnotationAppearanceStream(lineDef LineAnnotationDef) (*pdfcore.PdfObjectDictionary, *pdf.PdfRectangle, error) {
    75  	form := pdf.NewXObjectForm()
    76  	form.Resources = pdf.NewPdfPageResources()
    77  
    78  	gsName := ""
    79  	if lineDef.Opacity < 1.0 {
    80  		// Create graphics state with right opacity.
    81  		gsState := pdfcore.MakeDict()
    82  		gsState.Set("ca", pdfcore.MakeFloat(lineDef.Opacity))
    83  		err := form.Resources.AddExtGState("gs1", gsState)
    84  		if err != nil {
    85  			common.Log.Debug("Unable to add extgstate gs1")
    86  			return nil, nil, err
    87  		}
    88  
    89  		gsName = "gs1"
    90  	}
    91  
    92  	content, localBbox, globalBbox, err := drawPdfLine(lineDef, gsName)
    93  	if err != nil {
    94  		return nil, nil, err
    95  	}
    96  
    97  	err = form.SetContentStream(content, nil)
    98  	if err != nil {
    99  		return nil, nil, err
   100  	}
   101  
   102  	// Local bounding box for the XObject Form.
   103  	form.BBox = localBbox.ToPdfObject()
   104  
   105  	apDict := pdfcore.MakeDict()
   106  	apDict.Set("N", form.ToPdfObject())
   107  
   108  	return apDict, globalBbox, nil
   109  }
   110  
   111  func drawPdfLine(lineDef LineAnnotationDef, gsName string) ([]byte, *pdf.PdfRectangle, *pdf.PdfRectangle, error) {
   112  	// The annotation is drawn locally in a relative coordinate system with 0,0 as the origin rather than an offset.
   113  	line := draw.Line{
   114  		X1:               0,
   115  		Y1:               0,
   116  		X2:               lineDef.X2 - lineDef.X1,
   117  		Y2:               lineDef.Y2 - lineDef.Y1,
   118  		LineColor:        lineDef.LineColor,
   119  		Opacity:          lineDef.Opacity,
   120  		LineWidth:        lineDef.LineWidth,
   121  		LineEndingStyle1: lineDef.LineEndingStyle1,
   122  		LineEndingStyle2: lineDef.LineEndingStyle2,
   123  	}
   124  
   125  	content, localBbox, err := line.Draw(gsName)
   126  	if err != nil {
   127  		return nil, nil, nil, err
   128  	}
   129  
   130  	// Bounding box - global page coordinate system (with offset).
   131  	globalBbox := &pdf.PdfRectangle{}
   132  	globalBbox.Llx = lineDef.X1 + localBbox.Llx
   133  	globalBbox.Lly = lineDef.Y1 + localBbox.Lly
   134  	globalBbox.Urx = lineDef.X1 + localBbox.Urx
   135  	globalBbox.Ury = lineDef.Y1 + localBbox.Ury
   136  
   137  	return content, localBbox, globalBbox, nil
   138  }