...

Source file src/github.com/dsoprea/go-exif/v3/common/ifd.go

Documentation: github.com/dsoprea/go-exif/v3/common

     1  package exifcommon
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/dsoprea/go-logging"
     9  )
    10  
    11  var (
    12  	ifdLogger = log.NewLogger("exifcommon.ifd")
    13  )
    14  
    15  var (
    16  	ErrChildIfdNotMapped = errors.New("no child-IFD for that tag-ID under parent")
    17  )
    18  
    19  // MappedIfd is one node in the IFD-mapping.
    20  type MappedIfd struct {
    21  	ParentTagId uint16
    22  	Placement   []uint16
    23  	Path        []string
    24  
    25  	Name     string
    26  	TagId    uint16
    27  	Children map[uint16]*MappedIfd
    28  }
    29  
    30  // String returns a descriptive string.
    31  func (mi *MappedIfd) String() string {
    32  	pathPhrase := mi.PathPhrase()
    33  	return fmt.Sprintf("MappedIfd<(0x%04X) [%s] PATH=[%s]>", mi.TagId, mi.Name, pathPhrase)
    34  }
    35  
    36  // PathPhrase returns a non-fully-qualified IFD path.
    37  func (mi *MappedIfd) PathPhrase() string {
    38  	return strings.Join(mi.Path, "/")
    39  }
    40  
    41  // TODO(dustin): Refactor this to use IfdIdentity structs.
    42  
    43  // IfdMapping describes all of the IFDs that we currently recognize.
    44  type IfdMapping struct {
    45  	rootNode *MappedIfd
    46  }
    47  
    48  // NewIfdMapping returns a new IfdMapping struct.
    49  func NewIfdMapping() (ifdMapping *IfdMapping) {
    50  	rootNode := &MappedIfd{
    51  		Path:     make([]string, 0),
    52  		Children: make(map[uint16]*MappedIfd),
    53  	}
    54  
    55  	return &IfdMapping{
    56  		rootNode: rootNode,
    57  	}
    58  }
    59  
    60  // NewIfdMappingWithStandard retruns a new IfdMapping struct preloaded with the
    61  // standard IFDs.
    62  func NewIfdMappingWithStandard() (ifdMapping *IfdMapping, err error) {
    63  	defer func() {
    64  		if state := recover(); state != nil {
    65  			err = log.Wrap(state.(error))
    66  		}
    67  	}()
    68  
    69  	im := NewIfdMapping()
    70  
    71  	err = LoadStandardIfds(im)
    72  	log.PanicIf(err)
    73  
    74  	return im, nil
    75  }
    76  
    77  // Get returns the node given the path slice.
    78  func (im *IfdMapping) Get(parentPlacement []uint16) (childIfd *MappedIfd, err error) {
    79  	defer func() {
    80  		if state := recover(); state != nil {
    81  			err = log.Wrap(state.(error))
    82  		}
    83  	}()
    84  
    85  	ptr := im.rootNode
    86  	for _, tagId := range parentPlacement {
    87  		if descendantPtr, found := ptr.Children[tagId]; found == false {
    88  			log.Panicf("ifd child with tag-ID (%04x) not registered: [%s]", tagId, ptr.PathPhrase())
    89  		} else {
    90  			ptr = descendantPtr
    91  		}
    92  	}
    93  
    94  	return ptr, nil
    95  }
    96  
    97  // GetWithPath returns the node given the path string.
    98  func (im *IfdMapping) GetWithPath(pathPhrase string) (mi *MappedIfd, err error) {
    99  	defer func() {
   100  		if state := recover(); state != nil {
   101  			err = log.Wrap(state.(error))
   102  		}
   103  	}()
   104  
   105  	if pathPhrase == "" {
   106  		log.Panicf("path-phrase is empty")
   107  	}
   108  
   109  	path := strings.Split(pathPhrase, "/")
   110  	ptr := im.rootNode
   111  
   112  	for _, name := range path {
   113  		var hit *MappedIfd
   114  		for _, mi := range ptr.Children {
   115  			if mi.Name == name {
   116  				hit = mi
   117  				break
   118  			}
   119  		}
   120  
   121  		if hit == nil {
   122  			log.Panicf("ifd child with name [%s] not registered: [%s]", name, ptr.PathPhrase())
   123  		}
   124  
   125  		ptr = hit
   126  	}
   127  
   128  	return ptr, nil
   129  }
   130  
   131  // GetChild is a convenience function to get the child path for a given parent
   132  // placement and child tag-ID.
   133  func (im *IfdMapping) GetChild(parentPathPhrase string, tagId uint16) (mi *MappedIfd, err error) {
   134  	defer func() {
   135  		if state := recover(); state != nil {
   136  			err = log.Wrap(state.(error))
   137  		}
   138  	}()
   139  
   140  	mi, err = im.GetWithPath(parentPathPhrase)
   141  	log.PanicIf(err)
   142  
   143  	for _, childMi := range mi.Children {
   144  		if childMi.TagId == tagId {
   145  			return childMi, nil
   146  		}
   147  	}
   148  
   149  	// Whether or not an IFD is defined in data, such an IFD is not registered
   150  	// and would be unknown.
   151  	log.Panic(ErrChildIfdNotMapped)
   152  	return nil, nil
   153  }
   154  
   155  // IfdTagIdAndIndex represents a specific part of the IFD path.
   156  //
   157  // This is a legacy type.
   158  type IfdTagIdAndIndex struct {
   159  	Name  string
   160  	TagId uint16
   161  	Index int
   162  }
   163  
   164  // String returns a descriptive string.
   165  func (itii IfdTagIdAndIndex) String() string {
   166  	return fmt.Sprintf("IfdTagIdAndIndex<NAME=[%s] ID=(%04x) INDEX=(%d)>", itii.Name, itii.TagId, itii.Index)
   167  }
   168  
   169  // ResolvePath takes a list of names, which can also be suffixed with indices
   170  // (to identify the second, third, etc.. sibling IFD) and returns a list of
   171  // tag-IDs and those indices.
   172  //
   173  // Example:
   174  //
   175  // - IFD/Exif/Iop
   176  // - IFD0/Exif/Iop
   177  //
   178  // This is the only call that supports adding the numeric indices.
   179  func (im *IfdMapping) ResolvePath(pathPhrase string) (lineage []IfdTagIdAndIndex, err error) {
   180  	defer func() {
   181  		if state := recover(); state != nil {
   182  			err = log.Wrap(state.(error))
   183  		}
   184  	}()
   185  
   186  	pathPhrase = strings.TrimSpace(pathPhrase)
   187  
   188  	if pathPhrase == "" {
   189  		log.Panicf("can not resolve empty path-phrase")
   190  	}
   191  
   192  	path := strings.Split(pathPhrase, "/")
   193  	lineage = make([]IfdTagIdAndIndex, len(path))
   194  
   195  	ptr := im.rootNode
   196  	empty := IfdTagIdAndIndex{}
   197  	for i, name := range path {
   198  		indexByte := name[len(name)-1]
   199  		index := 0
   200  		if indexByte >= '0' && indexByte <= '9' {
   201  			index = int(indexByte - '0')
   202  			name = name[:len(name)-1]
   203  		}
   204  
   205  		itii := IfdTagIdAndIndex{}
   206  		for _, mi := range ptr.Children {
   207  			if mi.Name != name {
   208  				continue
   209  			}
   210  
   211  			itii.Name = name
   212  			itii.TagId = mi.TagId
   213  			itii.Index = index
   214  
   215  			ptr = mi
   216  
   217  			break
   218  		}
   219  
   220  		if itii == empty {
   221  			log.Panicf("ifd child with name [%s] not registered: [%s]", name, pathPhrase)
   222  		}
   223  
   224  		lineage[i] = itii
   225  	}
   226  
   227  	return lineage, nil
   228  }
   229  
   230  // FqPathPhraseFromLineage returns the fully-qualified IFD path from the slice.
   231  func (im *IfdMapping) FqPathPhraseFromLineage(lineage []IfdTagIdAndIndex) (fqPathPhrase string) {
   232  	fqPathParts := make([]string, len(lineage))
   233  	for i, itii := range lineage {
   234  		if itii.Index > 0 {
   235  			fqPathParts[i] = fmt.Sprintf("%s%d", itii.Name, itii.Index)
   236  		} else {
   237  			fqPathParts[i] = itii.Name
   238  		}
   239  	}
   240  
   241  	return strings.Join(fqPathParts, "/")
   242  }
   243  
   244  // PathPhraseFromLineage returns the non-fully-qualified IFD path from the
   245  // slice.
   246  func (im *IfdMapping) PathPhraseFromLineage(lineage []IfdTagIdAndIndex) (pathPhrase string) {
   247  	pathParts := make([]string, len(lineage))
   248  	for i, itii := range lineage {
   249  		pathParts[i] = itii.Name
   250  	}
   251  
   252  	return strings.Join(pathParts, "/")
   253  }
   254  
   255  // StripPathPhraseIndices returns a non-fully-qualified path-phrase (no
   256  // indices).
   257  func (im *IfdMapping) StripPathPhraseIndices(pathPhrase string) (strippedPathPhrase string, err error) {
   258  	defer func() {
   259  		if state := recover(); state != nil {
   260  			err = log.Wrap(state.(error))
   261  		}
   262  	}()
   263  
   264  	lineage, err := im.ResolvePath(pathPhrase)
   265  	log.PanicIf(err)
   266  
   267  	strippedPathPhrase = im.PathPhraseFromLineage(lineage)
   268  	return strippedPathPhrase, nil
   269  }
   270  
   271  // Add puts the given IFD at the given position of the tree. The position of the
   272  // tree is referred to as the placement and is represented by a set of tag-IDs,
   273  // where the leftmost is the root tag and the tags going to the right are
   274  // progressive descendants.
   275  func (im *IfdMapping) Add(parentPlacement []uint16, tagId uint16, name string) (err error) {
   276  	defer func() {
   277  		if state := recover(); state != nil {
   278  			err = log.Wrap(state.(error))
   279  		}
   280  	}()
   281  
   282  	// TODO(dustin): !! It would be nicer to provide a list of names in the placement rather than tag-IDs.
   283  
   284  	ptr, err := im.Get(parentPlacement)
   285  	log.PanicIf(err)
   286  
   287  	path := make([]string, len(parentPlacement)+1)
   288  	if len(parentPlacement) > 0 {
   289  		copy(path, ptr.Path)
   290  	}
   291  
   292  	path[len(path)-1] = name
   293  
   294  	placement := make([]uint16, len(parentPlacement)+1)
   295  	if len(placement) > 0 {
   296  		copy(placement, ptr.Placement)
   297  	}
   298  
   299  	placement[len(placement)-1] = tagId
   300  
   301  	childIfd := &MappedIfd{
   302  		ParentTagId: ptr.TagId,
   303  		Path:        path,
   304  		Placement:   placement,
   305  		Name:        name,
   306  		TagId:       tagId,
   307  		Children:    make(map[uint16]*MappedIfd),
   308  	}
   309  
   310  	if _, found := ptr.Children[tagId]; found == true {
   311  		log.Panicf("child IFD with tag-ID (%04x) already registered under IFD [%s] with tag-ID (%04x)", tagId, ptr.Name, ptr.TagId)
   312  	}
   313  
   314  	ptr.Children[tagId] = childIfd
   315  
   316  	return nil
   317  }
   318  
   319  func (im *IfdMapping) dumpLineages(stack []*MappedIfd, input []string) (output []string, err error) {
   320  	defer func() {
   321  		if state := recover(); state != nil {
   322  			err = log.Wrap(state.(error))
   323  		}
   324  	}()
   325  
   326  	currentIfd := stack[len(stack)-1]
   327  
   328  	output = input
   329  	for _, childIfd := range currentIfd.Children {
   330  		stackCopy := make([]*MappedIfd, len(stack)+1)
   331  
   332  		copy(stackCopy, stack)
   333  		stackCopy[len(stack)] = childIfd
   334  
   335  		// Add to output, but don't include the obligatory root node.
   336  		parts := make([]string, len(stackCopy)-1)
   337  		for i, mi := range stackCopy[1:] {
   338  			parts[i] = mi.Name
   339  		}
   340  
   341  		output = append(output, strings.Join(parts, "/"))
   342  
   343  		output, err = im.dumpLineages(stackCopy, output)
   344  		log.PanicIf(err)
   345  	}
   346  
   347  	return output, nil
   348  }
   349  
   350  // DumpLineages returns a slice of strings representing all mappings.
   351  func (im *IfdMapping) DumpLineages() (output []string, err error) {
   352  	defer func() {
   353  		if state := recover(); state != nil {
   354  			err = log.Wrap(state.(error))
   355  		}
   356  	}()
   357  
   358  	stack := []*MappedIfd{im.rootNode}
   359  	output = make([]string, 0)
   360  
   361  	output, err = im.dumpLineages(stack, output)
   362  	log.PanicIf(err)
   363  
   364  	return output, nil
   365  }
   366  
   367  // LoadStandardIfds loads the standard IFDs into the mapping.
   368  func LoadStandardIfds(im *IfdMapping) (err error) {
   369  	defer func() {
   370  		if state := recover(); state != nil {
   371  			err = log.Wrap(state.(error))
   372  		}
   373  	}()
   374  
   375  	err = im.Add(
   376  		[]uint16{},
   377  		IfdStandardIfdIdentity.TagId(), IfdStandardIfdIdentity.Name())
   378  
   379  	log.PanicIf(err)
   380  
   381  	err = im.Add(
   382  		[]uint16{IfdStandardIfdIdentity.TagId()},
   383  		IfdExifStandardIfdIdentity.TagId(), IfdExifStandardIfdIdentity.Name())
   384  
   385  	log.PanicIf(err)
   386  
   387  	err = im.Add(
   388  		[]uint16{IfdStandardIfdIdentity.TagId(), IfdExifStandardIfdIdentity.TagId()},
   389  		IfdExifIopStandardIfdIdentity.TagId(), IfdExifIopStandardIfdIdentity.Name())
   390  
   391  	log.PanicIf(err)
   392  
   393  	err = im.Add(
   394  		[]uint16{IfdStandardIfdIdentity.TagId()},
   395  		IfdGpsInfoStandardIfdIdentity.TagId(), IfdGpsInfoStandardIfdIdentity.Name())
   396  
   397  	log.PanicIf(err)
   398  
   399  	return nil
   400  }
   401  
   402  // IfdTag describes a single IFD tag and its parent (if any).
   403  type IfdTag struct {
   404  	parentIfdTag *IfdTag
   405  	tagId        uint16
   406  	name         string
   407  }
   408  
   409  func NewIfdTag(parentIfdTag *IfdTag, tagId uint16, name string) IfdTag {
   410  	return IfdTag{
   411  		parentIfdTag: parentIfdTag,
   412  		tagId:        tagId,
   413  		name:         name,
   414  	}
   415  }
   416  
   417  // ParentIfd returns the IfdTag of this IFD's parent.
   418  func (it IfdTag) ParentIfd() *IfdTag {
   419  	return it.parentIfdTag
   420  }
   421  
   422  // TagId returns the tag-ID of this IFD.
   423  func (it IfdTag) TagId() uint16 {
   424  	return it.tagId
   425  }
   426  
   427  // Name returns the simple name of this IFD.
   428  func (it IfdTag) Name() string {
   429  	return it.name
   430  }
   431  
   432  // String returns a descriptive string.
   433  func (it IfdTag) String() string {
   434  	parentIfdPhrase := ""
   435  	if it.parentIfdTag != nil {
   436  		parentIfdPhrase = fmt.Sprintf(" PARENT=(0x%04x)[%s]", it.parentIfdTag.tagId, it.parentIfdTag.name)
   437  	}
   438  
   439  	return fmt.Sprintf("IfdTag<TAG-ID=(0x%04x) NAME=[%s]%s>", it.tagId, it.name, parentIfdPhrase)
   440  }
   441  
   442  var (
   443  	// rootStandardIfd is the standard root IFD.
   444  	rootStandardIfd = NewIfdTag(nil, 0x0000, "IFD") // IFD
   445  
   446  	// exifStandardIfd is the standard "Exif" IFD.
   447  	exifStandardIfd = NewIfdTag(&rootStandardIfd, 0x8769, "Exif") // IFD/Exif
   448  
   449  	// iopStandardIfd is the standard "Iop" IFD.
   450  	iopStandardIfd = NewIfdTag(&exifStandardIfd, 0xA005, "Iop") // IFD/Exif/Iop
   451  
   452  	// gpsInfoStandardIfd is the standard "GPS" IFD.
   453  	gpsInfoStandardIfd = NewIfdTag(&rootStandardIfd, 0x8825, "GPSInfo") // IFD/GPSInfo
   454  )
   455  
   456  // IfdIdentityPart represents one component in an IFD path.
   457  type IfdIdentityPart struct {
   458  	Name  string
   459  	Index int
   460  }
   461  
   462  // String returns a fully-qualified IFD path.
   463  func (iip IfdIdentityPart) String() string {
   464  	if iip.Index > 0 {
   465  		return fmt.Sprintf("%s%d", iip.Name, iip.Index)
   466  	} else {
   467  		return iip.Name
   468  	}
   469  }
   470  
   471  // UnindexedString returned a non-fully-qualified IFD path.
   472  func (iip IfdIdentityPart) UnindexedString() string {
   473  	return iip.Name
   474  }
   475  
   476  // IfdIdentity represents a single IFD path and provides access to various
   477  // information and representations.
   478  //
   479  // Only global instances can be used for equality checks.
   480  type IfdIdentity struct {
   481  	ifdTag    IfdTag
   482  	parts     []IfdIdentityPart
   483  	ifdPath   string
   484  	fqIfdPath string
   485  }
   486  
   487  // NewIfdIdentity returns a new IfdIdentity struct.
   488  func NewIfdIdentity(ifdTag IfdTag, parts ...IfdIdentityPart) (ii *IfdIdentity) {
   489  	ii = &IfdIdentity{
   490  		ifdTag: ifdTag,
   491  		parts:  parts,
   492  	}
   493  
   494  	ii.ifdPath = ii.getIfdPath()
   495  	ii.fqIfdPath = ii.getFqIfdPath()
   496  
   497  	return ii
   498  }
   499  
   500  // NewIfdIdentityFromString parses a string like "IFD/Exif" or "IFD1" or
   501  // something more exotic with custom IFDs ("SomeIFD4/SomeChildIFD6"). Note that
   502  // this will valid the unindexed IFD structure (because the standard tags from
   503  // the specification are unindexed), but not, obviously, any indices (e.g.
   504  // the numbers in "IFD0", "IFD1", "SomeIFD4/SomeChildIFD6"). It is
   505  // required for the caller to check whether these specific instances
   506  // were actually parsed out of the stream.
   507  func NewIfdIdentityFromString(im *IfdMapping, fqIfdPath string) (ii *IfdIdentity, err error) {
   508  	defer func() {
   509  		if state := recover(); state != nil {
   510  			err = log.Wrap(state.(error))
   511  		}
   512  	}()
   513  
   514  	lineage, err := im.ResolvePath(fqIfdPath)
   515  	log.PanicIf(err)
   516  
   517  	var lastIt *IfdTag
   518  	identityParts := make([]IfdIdentityPart, len(lineage))
   519  	for i, itii := range lineage {
   520  		// Build out the tag that will eventually point to the IFD represented
   521  		// by the right-most part in the IFD path.
   522  
   523  		it := &IfdTag{
   524  			parentIfdTag: lastIt,
   525  			tagId:        itii.TagId,
   526  			name:         itii.Name,
   527  		}
   528  
   529  		lastIt = it
   530  
   531  		// Create the next IfdIdentity part.
   532  
   533  		iip := IfdIdentityPart{
   534  			Name:  itii.Name,
   535  			Index: itii.Index,
   536  		}
   537  
   538  		identityParts[i] = iip
   539  	}
   540  
   541  	ii = NewIfdIdentity(*lastIt, identityParts...)
   542  	return ii, nil
   543  }
   544  
   545  func (ii *IfdIdentity) getFqIfdPath() string {
   546  	partPhrases := make([]string, len(ii.parts))
   547  	for i, iip := range ii.parts {
   548  		partPhrases[i] = iip.String()
   549  	}
   550  
   551  	return strings.Join(partPhrases, "/")
   552  }
   553  
   554  func (ii *IfdIdentity) getIfdPath() string {
   555  	partPhrases := make([]string, len(ii.parts))
   556  	for i, iip := range ii.parts {
   557  		partPhrases[i] = iip.UnindexedString()
   558  	}
   559  
   560  	return strings.Join(partPhrases, "/")
   561  }
   562  
   563  // String returns a fully-qualified IFD path.
   564  func (ii *IfdIdentity) String() string {
   565  	return ii.fqIfdPath
   566  }
   567  
   568  // UnindexedString returns a non-fully-qualified IFD path.
   569  func (ii *IfdIdentity) UnindexedString() string {
   570  	return ii.ifdPath
   571  }
   572  
   573  // IfdTag returns the tag struct behind this IFD.
   574  func (ii *IfdIdentity) IfdTag() IfdTag {
   575  	return ii.ifdTag
   576  }
   577  
   578  // TagId returns the tag-ID of the IFD.
   579  func (ii *IfdIdentity) TagId() uint16 {
   580  	return ii.ifdTag.TagId()
   581  }
   582  
   583  // LeafPathPart returns the last right-most path-part, which represents the
   584  // current IFD.
   585  func (ii *IfdIdentity) LeafPathPart() IfdIdentityPart {
   586  	return ii.parts[len(ii.parts)-1]
   587  }
   588  
   589  // Name returns the simple name of this IFD.
   590  func (ii *IfdIdentity) Name() string {
   591  	return ii.LeafPathPart().Name
   592  }
   593  
   594  // Index returns the index of this IFD (more then one IFD under a parent IFD
   595  // will be numbered [0..n]).
   596  func (ii *IfdIdentity) Index() int {
   597  	return ii.LeafPathPart().Index
   598  }
   599  
   600  // Equals returns true if the two IfdIdentity instances are effectively
   601  // identical.
   602  //
   603  // Since there's no way to get a specific fully-qualified IFD path without a
   604  // certain slice of parts and all other fields are also derived from this,
   605  // checking that the fully-qualified IFD path is equals is sufficient.
   606  func (ii *IfdIdentity) Equals(ii2 *IfdIdentity) bool {
   607  	return ii.String() == ii2.String()
   608  }
   609  
   610  // NewChild creates an IfdIdentity for an IFD that is a child of the current
   611  // IFD.
   612  func (ii *IfdIdentity) NewChild(childIfdTag IfdTag, index int) (iiChild *IfdIdentity) {
   613  	if *childIfdTag.parentIfdTag != ii.ifdTag {
   614  		log.Panicf("can not add child; we are not the parent:\nUS=%v\nCHILD=%v", ii.ifdTag, childIfdTag)
   615  	}
   616  
   617  	childPart := IfdIdentityPart{childIfdTag.name, index}
   618  	childParts := append(ii.parts, childPart)
   619  
   620  	iiChild = NewIfdIdentity(childIfdTag, childParts...)
   621  	return iiChild
   622  }
   623  
   624  // NewSibling creates an IfdIdentity for an IFD that is a sibling to the current
   625  // one.
   626  func (ii *IfdIdentity) NewSibling(index int) (iiSibling *IfdIdentity) {
   627  	parts := make([]IfdIdentityPart, len(ii.parts))
   628  
   629  	copy(parts, ii.parts)
   630  	parts[len(parts)-1].Index = index
   631  
   632  	iiSibling = NewIfdIdentity(ii.ifdTag, parts...)
   633  	return iiSibling
   634  }
   635  
   636  var (
   637  	// IfdStandardIfdIdentity represents the IFD path for IFD0.
   638  	IfdStandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 0})
   639  
   640  	// IfdExifStandardIfdIdentity represents the IFD path for IFD0/Exif0.
   641  	IfdExifStandardIfdIdentity = IfdStandardIfdIdentity.NewChild(exifStandardIfd, 0)
   642  
   643  	// IfdExifIopStandardIfdIdentity represents the IFD path for IFD0/Exif0/Iop0.
   644  	IfdExifIopStandardIfdIdentity = IfdExifStandardIfdIdentity.NewChild(iopStandardIfd, 0)
   645  
   646  	// IfdGPSInfoStandardIfdIdentity represents the IFD path for IFD0/GPSInfo0.
   647  	IfdGpsInfoStandardIfdIdentity = IfdStandardIfdIdentity.NewChild(gpsInfoStandardIfd, 0)
   648  
   649  	// Ifd1StandardIfdIdentity represents the IFD path for IFD1.
   650  	Ifd1StandardIfdIdentity = NewIfdIdentity(rootStandardIfd, IfdIdentityPart{"IFD", 1})
   651  )
   652  

View as plain text