From ca7f479b7c6887ba926193995da221deab4edf04 Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 30 Jun 2020 21:34:03 +0300 Subject: [PATCH] Form fill field rotation (#385) * Refactor text field rotation * Add rotation support for checkbox fields * Add rotation support for combobox fields * Add rotation support for text combobox fields * Add documentation for the applyRotation of the AppearanceStyle --- annotator/field_appearance.go | 103 +++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/annotator/field_appearance.go b/annotator/field_appearance.go index fdacaaf6..7bdd8b60 100644 --- a/annotator/field_appearance.go +++ b/annotator/field_appearance.go @@ -219,15 +219,15 @@ func genFieldTextAppearance(wa *model.PdfAnnotationWidget, ftxt *model.PdfFieldT return nil, err } width, height := rect.Width(), rect.Height() + bboxWidth, bboxHeight := width, height - var rotation float64 - if mkDict, has := core.GetDict(wa.MK); has { + mkDict, has := core.GetDict(wa.MK) + if has { bsDict, _ := core.GetDict(wa.BS) err := style.applyAppearanceCharacteristics(mkDict, bsDict, nil) if err != nil { return nil, err } - rotation, _ = core.GetNumberAsFloat(mkDict.Get("R")) } // Get and process the default appearance string (DA) operands. @@ -252,26 +252,10 @@ func genFieldTextAppearance(wa *model.PdfAnnotationWidget, ftxt *model.PdfFieldT cc.Add_BMC("Tx") cc.Add_q() - bboxWidth, bboxHeight := width, height - if rotation != 0 { - // Calculate bounding box before rotation. - revRotation := -rotation - bbox := draw.Path{Points: []draw.Point{ - draw.NewPoint(0, 0).Rotate(revRotation), - draw.NewPoint(width, 0).Rotate(revRotation), - draw.NewPoint(0, height).Rotate(revRotation), - draw.NewPoint(width, height).Rotate(revRotation), - }}.GetBoundingBox() - - // Update width and height, as the appearance is generated based on - // the bounding of the annotation with no rotation. - width = bbox.Width - height = bbox.Height - - // Apply rotation. - cc.RotateDeg(rotation) - cc.Translate(bbox.X, bbox.Y) - } + // Apply rotation if present. + // Update width and height, as the appearance is generated based on + // the bounding of the annotation with no rotation. + width, height = style.applyRotation(mkDict, width, height, cc) // Graphic state changes. cc.Add_BT() @@ -513,8 +497,10 @@ func genFieldTextCombAppearance(wa *model.PdfAnnotationWidget, ftxt *model.PdfFi return nil, err } width, height := rect.Width(), rect.Height() + bboxWidth, bboxHeight := width, height - if mkDict, has := core.GetDict(wa.MK); has { + mkDict, has := core.GetDict(wa.MK) + if has { bsDict, _ := core.GetDict(wa.BS) err := style.applyAppearanceCharacteristics(mkDict, bsDict, nil) if err != nil { @@ -551,6 +537,11 @@ func genFieldTextCombAppearance(wa *model.PdfAnnotationWidget, ftxt *model.PdfFi cc.Add_BMC("Tx") cc.Add_q() + // Apply rotation if present. + // Update width and height, as the appearance is generated based on + // the bounding of the annotation with no rotation. + width, height = style.applyRotation(mkDict, width, height, cc) + // Graphic state changes. cc.Add_BT() @@ -680,7 +671,7 @@ func genFieldTextCombAppearance(wa *model.PdfAnnotationWidget, ftxt *model.PdfFi xform := model.NewXObjectForm() xform.Resources = resources - xform.BBox = core.MakeArrayFromFloats([]float64{0, 0, width, height}) + xform.BBox = core.MakeArrayFromFloats([]float64{0, 0, bboxWidth, bboxHeight}) xform.SetContentStream(cc.Bytes(), defStreamEncoder()) apDict := core.MakeDict() @@ -702,6 +693,7 @@ func genFieldCheckboxAppearance(wa *model.PdfAnnotationWidget, fbtn *model.PdfFi return nil, err } width, height := rect.Width(), rect.Height() + bboxWidth, bboxHeight := width, height common.Log.Debug("Checkbox, wa BS: %v", wa.BS) @@ -710,7 +702,8 @@ func genFieldCheckboxAppearance(wa *model.PdfAnnotationWidget, fbtn *model.PdfFi return nil, err } - if mkDict, has := core.GetDict(wa.MK); has { + mkDict, has := core.GetDict(wa.MK) + if has { bsDict, _ := core.GetDict(wa.BS) err := style.applyAppearanceCharacteristics(mkDict, bsDict, zapfdb) if err != nil { @@ -732,6 +725,11 @@ func genFieldCheckboxAppearance(wa *model.PdfAnnotationWidget, fbtn *model.PdfFi drawAlignmentReticle(cc, style2, width, height) } + // Apply rotation if present. + // Update width and height, as the appearance is generated based on + // the bounding of the annotation with no rotation. + width, height = style.applyRotation(mkDict, width, height, cc) + fontsize := style.AutoFontSizeFraction * height checkmetrics, ok := zapfdb.GetRuneMetrics(style.CheckmarkRune) @@ -767,7 +765,7 @@ func genFieldCheckboxAppearance(wa *model.PdfAnnotationWidget, fbtn *model.PdfFi xformOn.Resources = model.NewPdfPageResources() xformOn.Resources.SetFontByName("ZaDb", zapfdb.ToPdfObject()) - xformOn.BBox = core.MakeArrayFromFloats([]float64{0, 0, width, height}) + xformOn.BBox = core.MakeArrayFromFloats([]float64{0, 0, bboxWidth, bboxHeight}) xformOn.SetContentStream(cc.Bytes(), defStreamEncoder()) } @@ -777,7 +775,7 @@ func genFieldCheckboxAppearance(wa *model.PdfAnnotationWidget, fbtn *model.PdfFi if style.BorderSize > 0 { drawRect(cc, style, width, height) } - xformOff.BBox = core.MakeArrayFromFloats([]float64{0, 0, width, height}) + xformOff.BBox = core.MakeArrayFromFloats([]float64{0, 0, bboxWidth, bboxHeight}) xformOff.SetContentStream(cc.Bytes(), defStreamEncoder()) } @@ -813,7 +811,8 @@ func genFieldComboboxAppearance(form *model.PdfAcroForm, wa *model.PdfAnnotation return nil, err } - if mkDict, has := core.GetDict(wa.MK); has { + mkDict, has := core.GetDict(wa.MK) + if has { bsDict, _ := core.GetDict(wa.BS) err := style.applyAppearanceCharacteristics(mkDict, bsDict, nil) if err != nil { @@ -839,7 +838,7 @@ func genFieldComboboxAppearance(form *model.PdfAcroForm, wa *model.PdfAnnotation } if len(optstr) > 0 { - xform, err := makeComboboxTextXObjForm(fch.PdfField, width, height, optstr, style, daOps, form.DR) + xform, err := makeComboboxTextXObjForm(fch.PdfField, width, height, optstr, style, daOps, form.DR, mkDict) if err != nil { return nil, err } @@ -857,8 +856,9 @@ func genFieldComboboxAppearance(form *model.PdfAcroForm, wa *model.PdfAnnotation // Make a text-based XObj Form. func makeComboboxTextXObjForm(field *model.PdfField, width, height float64, text string, style AppearanceStyle, daOps *contentstream.ContentStreamOperations, - dr *model.PdfPageResources) (*model.XObjectForm, error) { + dr *model.PdfPageResources, mkDict *core.PdfObjectDictionary) (*model.XObjectForm, error) { resources := model.NewPdfPageResources() + bboxWidth, bboxHeight := width, height cc := contentstream.NewContentCreator() if style.BorderSize > 0 { @@ -875,6 +875,11 @@ func makeComboboxTextXObjForm(field *model.PdfField, width, height float64, // Graphic state changes. cc.Add_BT() + // Apply rotation if present. + // Update width and height, as the appearance is generated based on + // the bounding of the annotation with no rotation. + width, height = style.applyRotation(mkDict, width, height, cc) + // Process DA operands. apFont, hasTf, err := style.processDA(field, daOps, dr, resources, cc) if err != nil { @@ -950,7 +955,7 @@ func makeComboboxTextXObjForm(field *model.PdfField, width, height float64, xform := model.NewXObjectForm() xform.Resources = resources - xform.BBox = core.MakeArrayFromFloats([]float64{0, 0, width, height}) + xform.BBox = core.MakeArrayFromFloats([]float64{0, 0, bboxWidth, bboxHeight}) xform.SetContentStream(cc.Bytes(), defStreamEncoder()) return xform, nil @@ -1067,6 +1072,40 @@ func (style *AppearanceStyle) applyAppearanceCharacteristics(mkDict *core.PdfObj return nil } +// applyRotation applies the rotation specified by the MK dictionary, +// if present. The method returns the width and height of the annotation +// rectangle with no rotation. +func (style *AppearanceStyle) applyRotation(mkDict *core.PdfObjectDictionary, + width, height float64, cc *contentstream.ContentCreator) (float64, float64) { + if !style.AllowMK { + return width, height + } + if mkDict == nil { + return width, height + } + + // Extract rotation from the MK dictionary. + rotation, _ := core.GetNumberAsFloat(mkDict.Get("R")) + if rotation == 0 { + return width, height + } + + // Calculate bounding box before rotation. + revRotation := -rotation + bbox := draw.Path{Points: []draw.Point{ + draw.NewPoint(0, 0).Rotate(revRotation), + draw.NewPoint(width, 0).Rotate(revRotation), + draw.NewPoint(0, height).Rotate(revRotation), + draw.NewPoint(width, height).Rotate(revRotation), + }}.GetBoundingBox() + + // Apply rotation. + cc.RotateDeg(rotation) + cc.Translate(bbox.X, bbox.Y) + + return bbox.Width, bbox.Height +} + // processDA adds the operands found in the field default appearance stream to // the provided content stream creator. It also provides a fallback font, based // on the configuration of the AppearanceStyle, if no valid font is specified