├── LICENSE.md ├── README.md ├── demo └── extract.go └── fits.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Shahriar Iravanian (siravan@svtsim.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Copyright 2014 Shahriar Iravanian (siravan@svtsim.com). All rights reserved. 2 | Use of this source code is governed by a MIT license that can be found in the LICENSE file. 3 | 4 |
5 | Package fits reads and processes FITS files. It is written in pure golang and is not a wrapper around another library or a direct translation of 6 | another library to golang. The main purpose is to provide a native golang solution to reading FITS file and to assess the suitability of golang for 7 | scientific and numerical applications. 8 |
9 |10 | FITS is a common open-source format for storage and transmission of astronomical images and data. 11 | This package is based on version 3.0 of the FITS standard. 12 |
13 |14 | The following features are supported in the current version: 15 |
16 |1. Images with all six different data format (byte, int16, int32, int64, float32, and float64) 17 | 2. Text and binary tables with atomic and fixed-size array elements 18 |19 |
20 | The following features are not yet implemented: 21 |
22 |1. Automatic application of BSCALE/BZERO 23 | 2. Random group structure 24 | 3. Variable length arrays in binary tables 25 | 4. World coordinate system 26 |27 |
28 | Also note that currently this package provides only read capability and does not write/generate a FITS file. 29 |
30 |31 | The basic usage of the package is by calling Open function. It accepts a reader that should provide a valid FITS file. 32 | The output is a []*fits.Unit, where Unit represents a Header/Data Unit (i.e. a header with the corresponding data). 33 | Unit provides a set of variables and functions to access the HDU data. 34 |
35 |36 | Let 'test.fits' be a FITS file with two HDU. The first one is of type SIMPLE and contains a single two-dimensional image with the following parameters: 37 |
38 |BITPIX = -32 39 | NAXIS = 2 40 | NAXIS1 = 512 41 | NAXIS2 = 256 42 |43 |
44 | The second HDU contains a binary table (XTENSION=BINTABLE): 45 |
46 |BITPIX = 8 47 | NAXIS = 2 48 | NAXIS1 = 100 49 | NASIX2 = 5 50 | TFIELDS = 10 51 | TFORM1 = E 52 | TTYPE = FLUX 53 | TDISP1 = F10.4 54 |55 |
56 | To read this file, we first call 57 |
58 |units := fits.Open("test.fits")
59 |
60 | 61 | Now, units[0] points to the first HDU. We can access the header keys by using units.Keys map. 62 | For example, units[0].Keys["BITPIX"].(int) returns -32. Note that Keys stores interface{} and appropriate type-assertion needs to be done. 63 | Unit.Naxis returns a slice of integers ([]int) containing all NAXIS data. For example, units[0].Naxis is equal to [512, 256]. 64 | We can access the image data points by using one of the three accessor functions: Unit.At, Unit.IntAt and Unit.FloatAt. 65 | Each function accepts NAXIS integer arguments and returns the pixel value at that location. 66 | Unit.At returns an interface{} and needs to be type-asserted before use. Unit.IntAt and Unit.FloatAt return int64 and float64, respectively. 67 |
68 |69 | For table data, we use two other accessor functions: Field and Format. 70 | Field accepts one argument, col, that define a field. It can be 0-based int or a string. 71 | For example, units[1].Field(0) and units[1].Field("FLUX") both points to the same column. 72 | The return value of Field is another function, which is the actual accessor function and accepts one int argument representing a row. 73 | For example, units[1].Field("Flux")(1) returns the value of column "FLUX" in the second row of the table as interface{}. 74 | The following code populates a slice of float with the value of the FLUX column (note that Naxis[0] is the number of bytes in a row and Naxis[1] is the number of rows): 75 |
76 |fn := units[1].Field("FLUX")
77 | x := make([]float32, units[1].Naxis[1])
78 | for row := range x {
79 | x[row] = fn(row).(float32)
80 | }
81 |
82 | 83 | Format function on the hand accepts two arguments, col (same as Field) and row and return a string formatted according to TDISP for the field. 84 | For example, if units[1].Field("Flux")(1) is equal to 987.654321, then units[1].Format("Flux", 1) returns "987.6543".
85 | 86 | -------------------------------------------------------------------------------- /demo/extract.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Shahriar Iravanian (siravan@svtsim.com). All rights reserved. 2 | // Use of this source code is governed by a MIT license that can be found in the LICENSE file. 3 | // 4 | // extract is a test application for the fits (or go-fits) package. 5 | // It shows different usage of the fits package. 6 | // extract accepts one input in command line, which can be a file name or a URL pointing to a FITS file 7 | // extract reads the file and for each HDU writes the header key/value pairs to stdout 8 | // In addition, it extracts the data for each HDU and writes it in the relevant format 9 | // For images, the output format is 16-bit grayscale PNG 10 | // For tables, extract generates a text file containing the data 11 | // For one-dimensional images, it generates a simple one-column text file 12 | // 13 | package main 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "image" 19 | "image/color" 20 | "image/png" 21 | "io/ioutil" 22 | "log" 23 | "net/http" 24 | "net/url" 25 | "os" 26 | "path" 27 | "strings" 28 | "fits" 29 | ) 30 | 31 | func main() { 32 | var units []*fits.Unit 33 | var name string 34 | 35 | if len(os.Args) == 1 { 36 | fmt.Println("usage: extract filename|url") 37 | os.Exit(1) 38 | } 39 | 40 | if strings.HasPrefix(os.Args[1], "http://") { // called as "extract url" 41 | url, err := url.Parse(os.Args[1]) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | name = path.Base(url.Path) 46 | res, err := http.Get(os.Args[1]) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | buf, _ := ioutil.ReadAll(res.Body) // we download the whole FITS file first and then pass a buffered Reader to fits.Open 51 | res.Body.Close() 52 | units, err = fits.Open(bytes.NewReader(buf)) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | } else { // called as "extract filename" 57 | name = path.Base(os.Args[1]) 58 | reader, err := os.Open(os.Args[1]) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | defer reader.Close() 63 | units, err = fits.Open(reader) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | } 68 | 69 | j := strings.LastIndex(name, ".") // name is set to the name of the FITS file without its extension 70 | if j != -1 { 71 | name = string(name[:j]) 72 | } 73 | 74 | for i, h := range units { // for each HDU, extract the content 75 | fmt.Printf("******************** Header %d ********************\n", i) 76 | 77 | for key, value := range h.Keys { // First, write all key/value pairs 78 | fmt.Println(key, value) 79 | } 80 | 81 | out := fmt.Sprintf("%s_%d", name, i) 82 | 83 | if h.HasImage() { // Image type HDU (SIMPLE or XTENSION=IMAGE) 84 | writeImage(h, out) 85 | } else if h.HasTable() { // Table type HDU (XTENSION=TABLE or XTENSION=BINTABLE) 86 | if len(h.Naxis) == 1 { // One-dimensional table, write as an array 87 | writeArray(h, out) 88 | } else { // Two- or more dimensional, write as one or more PNG images 89 | writeTable(h, out) 90 | } 91 | } else { 92 | fmt.Println("Unsupported Header Data Unit") 93 | } 94 | } 95 | } 96 | 97 | // writeArray writes a one-dimensional image as a text file 98 | func writeArray(h *fits.Unit, name string) { 99 | g, _ := os.Create(name + ".dat") 100 | defer g.Close() 101 | 102 | for i := 0; i < h.Naxis[0]; i++ { 103 | fmt.Fprintln(g, h.FloatAt(i)) 104 | } 105 | } 106 | 107 | // writeImage generates PNG file(s) for image type HDUs 108 | // If h contains a two-dimensional image, a single 16-bit normalized PNG file is generated 109 | // For higher dimensional units, NAXIS3xNAXIS4x... different images (each NAXIS1xNAXIS2 in size) are generated 110 | // For example, if h.Naxis=[512, 512, 2, 3] and name is "test_0", the following images are written: 111 | // 112 | // test_0-0.0.png contains pixels [0,0,0,0] to [511,511,0,0] 113 | // test_0-1.0.png contains pixels [0,0,1,0] to [511,511,1,0] 114 | // test_0-0.1.png contains pixels [0,0,0,1] to [511,511,0,1] 115 | // test_0-1.1.png contains pixels [0,0,1,1] to [511,511,1,1] 116 | // test_0-0.2.png contains pixels [0,0,0,2] to [511,511,0,2] 117 | // test_0-1.2.png contains pixels [0,0,1,2] to [511,511,1,2] 118 | // 119 | func writeImage(h *fits.Unit, name string) { 120 | n := len(h.Naxis) 121 | maxis := make([]int, n) 122 | img := image.NewGray16(image.Rect(0, 0, h.Naxis[0], h.Naxis[1])) 123 | prod := 1 124 | for k := 2; k < n; k++ { 125 | prod *= h.Naxis[k] 126 | } 127 | min, max := h.Stats() 128 | 129 | for i := 0; i < prod; i++ { 130 | l := i 131 | s := name 132 | for k := 2; k < n; k++ { 133 | maxis[k] = l % h.Naxis[k] 134 | l = l / h.Naxis[k] 135 | s += fmt.Sprintf("-%d", maxis[k]) 136 | } 137 | 138 | for x := 0; x < h.Naxis[0]; x++ { 139 | for y := 0; y < h.Naxis[1]; y++ { 140 | maxis[0] = x 141 | maxis[1] = y 142 | if !h.Blank(maxis...) { 143 | v := uint16((h.FloatAt(maxis...) - min) / (max - min) * 65535) // normalizes based on min and max in the whole image cube 144 | img.SetGray16(x, h.Naxis[1]-y, color.Gray16{v}) 145 | } else { 146 | img.SetGray16(x, h.Naxis[1]-y, color.Gray16{0}) // blank pixel 147 | } 148 | } 149 | } 150 | 151 | g, _ := os.Create(s + ".png") 152 | defer g.Close() 153 | png.Encode(g, img) 154 | } 155 | } 156 | 157 | // writeTable generates a text file containing the table data of h 158 | // It processes both text (XTENSION=TABLE) and binary (XTENSION=BINTABLE) tables 159 | func writeTable(h *fits.Unit, name string) { 160 | g, _ := os.Create(name + ".tab") 161 | defer g.Close() 162 | ncols := h.Keys["TFIELDS"].(int) 163 | 164 | label := "" // label is the list of field names/labels 165 | for col := 0; col < ncols; col++ { 166 | ttype := h.Keys[fits.Nth("TTYPE", col+1)].(string) 167 | w := len(h.Format(col, 0)) // the label for each field is resized based on the size of the data on the first row of data 168 | label += fmt.Sprintf("%-*.*s", w, w, ttype) 169 | } 170 | fmt.Fprintln(g, label) 171 | 172 | for row := 0; row < h.Naxis[1]; row++ { 173 | s := "" 174 | for col := 0; col < ncols; col++ { 175 | s += h.Format(col, row) 176 | } 177 | fmt.Fprintln(g, s) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /fits.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Shahriar Iravanian (siravan@svtsim.com). All rights reserved. 2 | // Use of this source code is governed by a MIT license that can be found in the LICENSE file. 3 | // 4 | // Package fits reads and processes FITS files. It is written in pure golang and is not a wrapper around another library or a direct translation of 5 | // another library to golang. The main purpose is to provide a native golang solution to reading FITS file and to assess the suitability of golang for 6 | // scientific and numerical applications. 7 | // 8 | // FITS is a common format for astronomical image and data. 9 | // This package is based on version 3.0 of the FITS standard: 10 | // Pence W.D., Chiappetti L., Page C. G., Shaw R. A., Stobie E. Definition of the Flexible Image Transport System (FITS), version 3.0. A&A 524, A42 (2010) 11 | // http://www.aanda.org/articles/aa/abs/2010/16/aa15362-10/aa15362-10.html 12 | // 13 | // The following features are supported in the current version: 14 | // 1. Images with all six different data format (byte, int16, int32, int64, float32, and float64) 15 | // 2. Text and binary tables with atomic and fixed-size array elements 16 | // 17 | // The following features are not yet implemented: 18 | // 1. Automatic application of BSCALE/BZERO 19 | // 2. Random group structure 20 | // 3. Variable length arrays in binary tables 21 | // 4. World coordinate system 22 | // 23 | // Also note that currently this package provides only read capability and does not write/generate a FITS file. 24 | // 25 | // The basic usage of the package is by calling Open function. It accepts a reader that should provide a valid FITS file. 26 | // The output is a []*fits.Unit, where Unit represents a Header/Data Unit (i.e. a header with the corresponding data). 27 | // Unit provides a set of variables and functions to access the HDU data. 28 | // 29 | // Let 'test.fits' be a FITS file with two HDU. The first one is of type SIMPLE and contains a single two-dimensional image with the following parameters: 30 | // 31 | // BITPIX = -32 32 | // NAXIS = 2 33 | // NAXIS1 = 512 34 | // NAXIS2 = 256 35 | // 36 | // The second HDU contains a binary table (XTENSION=BINTABLE): 37 | // 38 | // BITPIX = 8 39 | // NAXIS = 2 40 | // NAXIS1 = 100 41 | // NASIX2 = 5 42 | // TFIELDS = 10 43 | // TFORM1 = E 44 | // TTYPE = FLUX 45 | // TDISP1 = F10.4 46 | // 47 | // To read this file, we first call 48 | // 49 | // units := fits.Open("test.fits") 50 | // 51 | // Now, units[0] points to the first HDU. We can access the header keys by using units.Keys map. 52 | // For example, units[0].Keys["BITPIX"].(int) returns -32. Note that Keys stores interface{} and appropriate type-assertion needs to be done. 53 | // Unit.Naxis returns a slice of integers ([]int) containing all NAXIS data. For example, units[0].Naxis is equal to [512, 256]. 54 | // We can access the image data points by using one of the three accessor functions: Unit.At, Unit.IntAt and Unit.FloatAt. 55 | // Each function accepts NAXIS integer arguments and returns the pixel value at that location. 56 | // Unit.At returns an interface{} and needs to be type-asserted before use. Unit.IntAt and Unit.FloatAt return int64 and float64, respectively. 57 | // 58 | // For table data, we use two other accessor functions: Field and Format. 59 | // Field accepts one argument, col, that define a field. It can be 0-based int or a string. 60 | // For example, units[1].Field(0) and units[1].Field("FLUX") both points to the same column. 61 | // The return value of Field is another function, which is the actual accessor function and accepts one int argument representing a row. 62 | // For example, units[1].Field("Flux")(1) returns the value of column "FLUX" in the second row of the table as interface{}. 63 | // The following code populates a slice of float with the value of the FLUX column: 64 | // 65 | // fn := units[1].Field("FLUX") 66 | // x := make([]float32, units[1].Naxis[1]) // note Naxis[0]=NAXIS1=length of a row, Naxis[1]=NAXIS2=number of rows 67 | // for row := range x { 68 | // x[row] = fn(row).(float32) 69 | // } 70 | // 71 | // Format function on the hand accepts two arguments, col (same as Field) and row and return a string formatted according to TDISP for the field. 72 | // For example, if units[1].Field("Flux")(1) is equal to 987.654321, then units[1].Format("Flux", 1) returns "987.6543". 73 | // 74 | package fits 75 | 76 | import ( 77 | "bytes" 78 | "fmt" 79 | "io" 80 | "math" 81 | "strconv" 82 | "strings" 83 | "sync" 84 | ) 85 | 86 | // FieldFunc are the type of accessor functions returned by Unit.Field() 87 | // FieldFunc is used to access the value of cells in a text or binary table (XTENSION=TABLE or XTENSION=BINTABLE) 88 | type FieldFunc func(row int) interface{} 89 | 90 | // Unit stored the header and data of a single HDU (Header Data Unit) as defined by FITS standard 91 | // Data points to a flat array holding the HDU data 92 | // Its type is []byte for tables and is determined by BITPIX for images: 93 | // 94 | // BITPIX Data 95 | // 8 []byte 96 | // 16 []int16 97 | // 32 []int32 98 | // 64 []int64 99 | // -32 []float32 100 | // -64 []float64 101 | // 102 | type Unit struct { 103 | Keys map[string]interface{} 104 | Naxis []int // len(Naxis) is equal to the value of NASIX in the header 105 | // Naxis[k] is equal to NAXIS{k+1} in the header 106 | Data interface{} 107 | list []FieldFunc // A slice to help with access to FieldFunc based on index 108 | fields map[string]FieldFunc // A map of FieldFunc (field-name => accessor-function) 109 | // field-name is based on TTYPE{k} keys in the header 110 | class string // class holds the type of the Header (SIMPLE, IMAGE, TABLE and BINTABLE) 111 | blank int // The value of BLANK key in the header 112 | At func(a ...int) interface{} // Accessor function that returns the value of a pixel based on its coordinates 113 | // a... represents NAXIS integers corresponding to NAXIS1, NAXIS2,... 114 | // The return result type is interface{}. The concrete type is determined by BITPIX 115 | IntAt func(a ...int) int64 // A helper accessor function that returns the pixel value as int64 116 | FloatAt func(a ...int) float64 // A helper accessor function that returns the pixel value as float64 117 | Blank func(a ...int) bool // returns true if pixel type is integral and the pixel pointed by a... is equal to blank, 118 | // or the pixel type is float and its value is NaN 119 | } 120 | 121 | // Reader is a buffered Reader implementation that works based on the FITS block structure (each 2880 bytes long) 122 | type Reader struct { 123 | buf []byte 124 | elem []byte 125 | left int 126 | right int 127 | reader io.Reader 128 | eof bool 129 | } 130 | 131 | // Field returns a FieldFunc corresponding to col 132 | // If col is int, the col'th field is returned (note: col is 0 based, so col=1 means TFORM2) 133 | // If col a string, the field with TDISP equal to col is returned 134 | // Fields are held in a map (Unit.fields) based on their name (TDISP). 135 | // In addition, for each field, an entry with key "#name" is added to Unit.fields to facilitate the search for TDISP based on the name 136 | // 137 | // Note: this function returns an accessor function, that needs to be called to obtain the actual cell value 138 | // For example, assume h is a table. One of its column is named "ID" of type "J" (int32) 139 | // To obtain the value of the cell located at the intersection of the third row (row=2) and column "ID", we write 140 | // 141 | // fn := h.Field("ID") 142 | // val := fn(2).(int32) 143 | // 144 | func (h *Unit) Field(col interface{}) FieldFunc { 145 | var x FieldFunc 146 | var ok bool 147 | 148 | switch col.(type) { 149 | case int: 150 | n := col.(int) 151 | if n >= 0 && n < len(h.list) { 152 | return h.list[col.(int)] 153 | } 154 | case string: 155 | x, ok = h.fields[col.(string)] 156 | if ok { 157 | return x 158 | } 159 | } 160 | return func(int) interface{} { 161 | return nil 162 | } 163 | } 164 | 165 | // Format returns a formatted string based on the given col and row and TDISP of the col 166 | // col can be an int or a string (same as Field) 167 | // The return value is a string, which is obtained by 168 | // 1. Finding the FieldFunc based on col 169 | // 2. Running the FieldFunc by passing row as an argument 170 | // 3. Applying format to the result 171 | // 172 | func (h *Unit) Format(col interface{}, row int) string { 173 | var fn FieldFunc 174 | var disp interface{} 175 | 176 | switch col.(type) { 177 | case int: 178 | n := col.(int) 179 | if n >= 0 && n < len(h.list) { 180 | fn = h.list[col.(int)] 181 | disp, _ = h.Keys[Nth("TDISP", n+1)] 182 | } 183 | case string: 184 | name := col.(string) 185 | fn, _ = h.fields[name] 186 | n := h.Keys["#"+name] 187 | disp, _ = h.Keys[Nth("TDISP", n.(int))] 188 | } 189 | 190 | if fn == nil { 191 | return "" 192 | } 193 | 194 | format := "%v" // default format 195 | 196 | w := 14 197 | if disp != nil { 198 | var code rune 199 | m := -1 200 | d := disp.(string) 201 | 202 | // accounts for ENw.d and ESw.d formats 203 | if len(d) > 1 && (d[1] == 'N' || d[1] == 'S') { 204 | d = string(d[0]) + string(d[2:]) // removes the second character from the format string 205 | // The standard allows to disregard this secondary format characters 206 | } 207 | 208 | fmt.Sscanf(d, "%c%d.%d", &code, &w, &m) 209 | 210 | switch code { 211 | case 'A': 212 | format = fmt.Sprintf("%%%d.%ds", w, w) // Aw -> %ws 213 | case 'I': 214 | format = fmt.Sprintf("%%%dd", w) // Iw -> %wd 215 | case 'B': 216 | format = fmt.Sprintf("%%%db", w) // Bw -> %wb, binary 217 | case 'O': 218 | format = fmt.Sprintf("%%%do", w) // Ow -> %wo, octal 219 | case 'Z': 220 | format = fmt.Sprintf("%%%dX", w) // Zw -> %wX, hexadecimal 221 | case 'F', 'D': 222 | if m != -1 { 223 | format = fmt.Sprintf("%%%d.%df", w, m) // Fw.d -> %w.df 224 | } else { 225 | format = fmt.Sprintf("%%%df", w) // Fw -> %wf 226 | } 227 | case 'E': 228 | if m != -1 { 229 | format = fmt.Sprintf("%%%d.%de", w, m) // Fw.d -> %w.df 230 | } else { 231 | format = fmt.Sprintf("%%%de", w) // Ew -> %we 232 | } 233 | case 'G': 234 | if m != -1 { 235 | format = fmt.Sprintf("%%%d.%dg", w, m) // Fw.d -> %w.df 236 | } else { 237 | format = fmt.Sprintf("%%%dg", w) // Gw -> %wg 238 | } 239 | } 240 | } 241 | 242 | return fmt.Sprintf(format, fn(row)) 243 | } 244 | 245 | // HasImage returns true is the Unit is either SIMPLE or IMAGE and has the data for an actual image 246 | func (h *Unit) HasImage() bool { 247 | return (h.class == "SIMPLE" || h.class == "IMAGE") && len(h.Naxis) > 0 && h.Naxis[0] > 0 248 | } 249 | 250 | // HasImage returns true is the Unit is either TABLE or BINTABLE and has the data for an actual table 251 | func (h *Unit) HasTable() bool { 252 | return (h.class == "TABLE" || h.class == "BINTABLE") 253 | } 254 | 255 | // Bitpix is a helper function the simply returns BITPIX value in the header 256 | func (h *Unit) Bitpix() int { 257 | return h.Keys["BITPIX"].(int) 258 | } 259 | 260 | // Stats returns the minimum and maximum values in the image data 261 | func (h *Unit) Stats() (min float64, max float64) { 262 | prod := 1 263 | for _, x := range h.Naxis { 264 | prod *= x 265 | } 266 | if prod == 1 { 267 | return 268 | } 269 | 270 | min = math.MaxFloat64 271 | max = -math.MaxFloat64 272 | 273 | switch h.Bitpix() { 274 | case 8: 275 | for i := 0; i < prod; i++ { 276 | x := int(h.Data.([]byte)[i]) 277 | if x != h.blank && float64(x) < min { 278 | min = float64(x) 279 | } 280 | if x != h.blank && float64(x) > max { 281 | max = float64(x) 282 | } 283 | } 284 | case 16: 285 | for i := 0; i < prod; i++ { 286 | x := int(h.Data.([]int16)[i]) 287 | if x != h.blank && float64(x) < min { 288 | min = float64(x) 289 | } 290 | if x != h.blank && float64(x) > max { 291 | max = float64(x) 292 | } 293 | } 294 | case 32: 295 | for i := 0; i < prod; i++ { 296 | x := int(h.Data.([]int32)[i]) 297 | if x != h.blank && float64(x) < min { 298 | min = float64(x) 299 | } 300 | if x != h.blank && float64(x) > max { 301 | max = float64(x) 302 | } 303 | } 304 | case 64: 305 | for i := 0; i < prod; i++ { 306 | x := int(h.Data.([]int64)[i]) 307 | if x != h.blank && float64(x) < min { 308 | min = float64(x) 309 | } 310 | if x != h.blank && float64(x) > max { 311 | max = float64(x) 312 | } 313 | } 314 | case -32: 315 | for i := 0; i < prod; i++ { 316 | x := float64(h.Data.([]float32)[i]) 317 | if !math.IsNaN(x) && x < min { 318 | min = x 319 | } 320 | if !math.IsNaN(x) && x > max { 321 | max = x 322 | } 323 | } 324 | case -64: 325 | for i := 0; i < prod; i++ { 326 | x := h.Data.([]float64)[i] 327 | if !math.IsNaN(x) && x < min { 328 | min = x 329 | } 330 | if !math.IsNaN(x) && x > max { 331 | max = x 332 | } 333 | } 334 | } 335 | return 336 | } 337 | 338 | // Open processes a FITS file provided as an io.Reader and returns a list of HDUs in the FITS file 339 | // It is the main entry point of the fits package 340 | func Open(reader io.Reader) (fits []*Unit, err error) { 341 | b := NewReader(reader) 342 | fits = make([]*Unit, 0, 5) 343 | done: 344 | for !b.IsEOF() { 345 | h, err := b.NewHeader() 346 | if err != nil { 347 | err = nil // EOF, not an error? 348 | break 349 | } 350 | fits = append(fits, h) 351 | if _, ok := h.Keys["SIMPLE"]; ok { 352 | err = h.verifyPrimary() 353 | if err != nil { 354 | break 355 | } 356 | h.class = "SIMPLE" 357 | if len(h.Naxis) > 0 { 358 | if h.Naxis[0] == 0 { // Random Group Headers are not supported and are not processed further 359 | break done 360 | } 361 | err = h.loadData(b) // Imaging data 362 | if err != nil { 363 | break 364 | } 365 | } 366 | } else if xten, ok := h.Keys["XTENSION"].(string); ok { 367 | err = h.verifyExtension() 368 | if err != nil { 369 | break 370 | } 371 | h.class = xten 372 | switch xten { 373 | case "IMAGE": 374 | if len(h.Naxis) > 0 { 375 | err = h.loadData(b) 376 | if err != nil { 377 | break 378 | } 379 | } 380 | case "TABLE": 381 | err = h.loadTable(b, false) 382 | if err != nil { 383 | break 384 | } 385 | case "BINTABLE": 386 | err = h.loadTable(b, true) 387 | if err != nil { 388 | break 389 | } 390 | } 391 | } else { 392 | // unknown header 393 | break 394 | } 395 | } 396 | return fits, err 397 | } 398 | 399 | // index is a helper function the returns the index of the pixel pointed by a... in a flat Data array 400 | func (h *Unit) index(a ...int) int { 401 | var index int 402 | for i := len(h.Naxis) - 1; i >= 0; i-- { 403 | index = index*h.Naxis[i] + a[i] 404 | } 405 | return index 406 | } 407 | 408 | // loadData processes the image type data sections 409 | // It allocates Data, populates it, and sets the appropriate pixel accessor functions 410 | func (h *Unit) loadData(b *Reader) error { 411 | var i int 412 | 413 | if len(h.Naxis) == 0 { 414 | h.Data = make([]int, 0) 415 | h.IntAt = func(a ...int) int64 { 416 | return 0 417 | } 418 | h.FloatAt = func(a ...int) float64 { 419 | return 0 420 | } 421 | return nil 422 | } 423 | 424 | prod := 1 425 | for _, x := range h.Naxis { 426 | prod *= x 427 | } 428 | 429 | bitpix := h.Keys["BITPIX"].(int) 430 | 431 | switch bitpix { 432 | case 8: 433 | data := make([]byte, prod) // Data type is determined based on bitpix 434 | h.Data = data 435 | h.At = func(a ...int) interface{} { // The accessor functions look similar, but note that data is redefined and has a different type for each case 436 | // Templates (generics) would have helped with cutting back on redundant code! 437 | return data[h.index(a...)] 438 | } 439 | h.IntAt = func(a ...int) int64 { 440 | return int64(data[h.index(a...)]) 441 | } 442 | h.FloatAt = func(a ...int) float64 { 443 | return float64(data[h.index(a...)]) 444 | } 445 | for i = 0; i < prod; i++ { 446 | data[i] = b.ReadByte() 447 | } 448 | case 16: 449 | data := make([]int16, prod) 450 | h.Data = data 451 | h.At = func(a ...int) interface{} { 452 | return data[h.index(a...)] 453 | } 454 | h.IntAt = func(a ...int) int64 { 455 | return int64(data[h.index(a...)]) 456 | } 457 | h.FloatAt = func(a ...int) float64 { 458 | return float64(data[h.index(a...)]) 459 | } 460 | for i = 0; i < prod; i++ { 461 | data[i] = b.ReadInt16() 462 | } 463 | case 32: 464 | data := make([]int32, prod) 465 | h.Data = data 466 | h.At = func(a ...int) interface{} { 467 | return data[h.index(a...)] 468 | } 469 | h.IntAt = func(a ...int) int64 { 470 | return int64(data[h.index(a...)]) 471 | } 472 | h.FloatAt = func(a ...int) float64 { 473 | return float64(data[h.index(a...)]) 474 | } 475 | for i = 0; i < prod; i++ { 476 | data[i] = b.ReadInt32() 477 | } 478 | case 64: 479 | data := make([]int64, prod) 480 | h.Data = data 481 | h.At = func(a ...int) interface{} { 482 | return data[h.index(a...)] 483 | } 484 | h.IntAt = func(a ...int) int64 { 485 | return int64(data[h.index(a...)]) 486 | } 487 | h.FloatAt = func(a ...int) float64 { 488 | return float64(data[h.index(a...)]) 489 | } 490 | for i = 0; i < prod; i++ { 491 | data[i] = b.ReadInt64() 492 | } 493 | case -32: 494 | data := make([]float32, prod) 495 | h.Data = data 496 | h.At = func(a ...int) interface{} { 497 | return data[h.index(a...)] 498 | } 499 | h.IntAt = func(a ...int) int64 { 500 | return int64(data[h.index(a...)]) 501 | } 502 | h.FloatAt = func(a ...int) float64 { 503 | return float64(data[h.index(a...)]) 504 | } 505 | for i = 0; i < prod; i++ { 506 | data[i] = b.ReadFloat32() 507 | } 508 | case -64: 509 | data := make([]float64, prod) 510 | h.Data = data 511 | h.At = func(a ...int) interface{} { 512 | return data[h.index(a...)] 513 | } 514 | h.IntAt = func(a ...int) int64 { 515 | return int64(data[h.index(a...)]) 516 | } 517 | h.FloatAt = func(a ...int) float64 { 518 | return float64(data[h.index(a...)]) 519 | } 520 | for i = 0; i < prod; i++ { 521 | data[i] = b.ReadFloat64() 522 | } 523 | } 524 | 525 | blank, ok := h.Keys["BLANK"] 526 | switch { 527 | case ok && bitpix > 0: // Integer pixel type with defined BLANK 528 | h.blank = blank.(int) 529 | h.Blank = func(a ...int) bool { 530 | return h.IntAt(a...) == int64(h.blank) 531 | } 532 | case bitpix < 0: // Float pixel type 533 | h.Blank = func(a ...int) bool { 534 | return math.IsNaN(h.FloatAt(a...)) 535 | } 536 | default: // Integer pixel type with undefined BLANK 537 | h.Blank = func(a ...int) bool { 538 | return false 539 | } 540 | } 541 | 542 | return nil 543 | } 544 | 545 | // accessorBin generates the accessor function for a field in a binary table (XTENSION=BINTABLE) 546 | // loadTable function processes TFORM for each field 547 | // For binary tables, TFORM is like rT, where r is the repeat and T is the type code 548 | // With the exception of code='A' (string-type), the accessor functions are different for repeat=1 (returns an atomic value) vs repeat>1 (returns a fixed array) 549 | // Note, variable arrays (type P and Q) and packed bits (type X) are not supported in the current version 550 | // col is the byte index of the value of the field from the beginning of each record 551 | func (h *Unit) accessorBin(code byte, repeat int, col *int) (fn func(int) interface{}, disp string) { 552 | c := *col 553 | l := 0 554 | var f func() interface{} // f holds a helper function that returns the field data assuming that b is set correctly 555 | 556 | // we use a fits.Reader to access data values in the binary table 557 | b := new(Reader) 558 | b.buf = h.Data.([]byte) 559 | b.elem = make([]byte, 8) 560 | b.right = len(b.buf) 561 | 562 | switch code { 563 | case 'A': 564 | f = func() interface{} { // For T='A', the result is always a string, even if repeat is equal to 1 565 | return b.ReadString(repeat) 566 | } 567 | l = 1 568 | disp = fmt.Sprintf("A%d", repeat) 569 | case 'B': 570 | if repeat == 1 { 571 | f = func() interface{} { 572 | return b.ReadByte() 573 | } 574 | } else { 575 | f = func() interface{} { 576 | p := make([]uint8, repeat) 577 | for i := 0; i < repeat; i++ { 578 | p[i] = b.ReadByte() 579 | } 580 | return p 581 | } 582 | } 583 | l = 1 584 | disp = "I3" // disp is the default display formatting string to be used if the corresponding TDISP is missing 585 | case 'L': 586 | if repeat == 1 { 587 | f = func() interface{} { 588 | return b.ReadBool() 589 | } 590 | } else { 591 | f = func() interface{} { 592 | p := make([]bool, repeat) 593 | for i := 0; i < repeat; i++ { 594 | p[i] = b.ReadBool() 595 | } 596 | return p 597 | } 598 | } 599 | l = 1 600 | disp = "B1" 601 | case 'I': 602 | if repeat == 1 { 603 | f = func() interface{} { 604 | return b.ReadInt16() 605 | } 606 | } else { 607 | f = func() interface{} { 608 | p := make([]int16, repeat) 609 | for i := 0; i < repeat; i++ { 610 | p[i] = b.ReadInt16() 611 | } 612 | return p 613 | } 614 | } 615 | l = 2 616 | disp = "I6" 617 | case 'J': 618 | if repeat == 1 { 619 | f = func() interface{} { 620 | return b.ReadInt32() 621 | } 622 | } else { 623 | f = func() interface{} { 624 | p := make([]int32, repeat) 625 | for i := 0; i < repeat; i++ { 626 | p[i] = b.ReadInt32() 627 | } 628 | return p 629 | } 630 | } 631 | l = 4 632 | disp = "I11" 633 | case 'K': 634 | if repeat == 1 { 635 | f = func() interface{} { 636 | return b.ReadInt64() 637 | } 638 | } else { 639 | f = func() interface{} { 640 | p := make([]int64, repeat) 641 | for i := 0; i < repeat; i++ { 642 | p[i] = b.ReadInt64() 643 | } 644 | return p 645 | } 646 | } 647 | l = 8 648 | disp = "I20" 649 | case 'D': 650 | if repeat == 1 { 651 | f = func() interface{} { 652 | return b.ReadFloat64() 653 | } 654 | } else { 655 | f = func() interface{} { 656 | p := make([]float64, repeat) 657 | for i := 0; i < repeat; i++ { 658 | p[i] = b.ReadFloat64() 659 | } 660 | return p 661 | } 662 | } 663 | l = 8 664 | disp = "F14.7" 665 | case 'E': 666 | if repeat == 1 { 667 | f = func() interface{} { 668 | return b.ReadFloat32() 669 | } 670 | } else { 671 | f = func() interface{} { 672 | p := make([]float32, repeat) 673 | for i := 0; i < repeat; i++ { 674 | p[i] = b.ReadFloat32() 675 | } 676 | return p 677 | } 678 | } 679 | 680 | l = 4 681 | disp = "F14.7" 682 | case 'M': 683 | if repeat == 1 { 684 | f = func() interface{} { 685 | x := b.ReadFloat64() 686 | y := b.ReadFloat64() 687 | return complex(x, y) 688 | } 689 | } else { 690 | f = func() interface{} { 691 | p := make([]complex128, repeat) 692 | for i := 0; i < repeat; i++ { 693 | x := b.ReadFloat64() 694 | y := b.ReadFloat64() 695 | p[i] = complex(x, y) 696 | } 697 | return p 698 | } 699 | } 700 | l = 16 701 | disp = "F14.7" 702 | case 'C': 703 | if repeat == 1 { 704 | f = func() interface{} { 705 | x := b.ReadFloat32() 706 | y := b.ReadFloat32() 707 | return complex(x, y) 708 | } 709 | } else { 710 | f = func() interface{} { 711 | p := make([]complex64, repeat) 712 | for i := 0; i < repeat; i++ { 713 | x := b.ReadFloat32() 714 | y := b.ReadFloat32() 715 | p[i] = complex(x, y) 716 | } 717 | return p 718 | } 719 | } 720 | l = 8 721 | disp = "F14.7" 722 | case 'X', 'P', 'Q': 723 | panic("Binary table forms X, P and Q are not supported") 724 | } 725 | 726 | *col += l * repeat 727 | 728 | // fn is the actual FieldFunc 729 | // it sets b.left based on the record size and row and calls f to extract the field value 730 | fn = func(row int) interface{} { 731 | var m sync.Mutex 732 | m.Lock() // Lock is needed because each FieldFunc closes over a fits.Reader and b.left is modified 733 | if row < 0 || row >= h.Naxis[1] { // invalid row number (note Naxis[1] is NAXIS2 in the header equal to the number of rows) 734 | return nil 735 | } 736 | b.left = row*h.Naxis[0] + c 737 | x := f() 738 | m.Unlock() 739 | return x 740 | } 741 | 742 | return fn, disp 743 | } 744 | 745 | // accessorText generates the accessor function for a field in a text table (XTENSION=TABLE) 746 | // loadTable function processes TFORM for each field 747 | // For text tables, TFORM is like Tw or Tw.d (T=code and w=repeat) 748 | func (h *Unit) accessorText(code byte, repeat int, col *int) (fn func(int) interface{}, disp string) { 749 | c := *col - 1 750 | var f func() interface{} 751 | b := new(Reader) // note that b.elem does not need to be set because we only use b.ReadString 752 | b.buf = h.Data.([]byte) 753 | b.right = len(b.buf) 754 | 755 | switch code { 756 | case 'A': 757 | f = func() interface{} { 758 | return b.ReadString(repeat) 759 | } 760 | disp = fmt.Sprintf("A%d", repeat) 761 | case 'I': 762 | f = func() interface{} { 763 | s := b.ReadString(repeat) 764 | s = strings.TrimSpace(s) 765 | n, _ := strconv.ParseInt(s, 10, 32) 766 | return int(n) 767 | } 768 | disp = fmt.Sprintf("I%d", repeat) 769 | case 'D', 'E', 'F': 770 | f = func() interface{} { 771 | s := b.ReadString(repeat) 772 | s = strings.TrimSpace(s) 773 | s = strings.Replace(s, "D", "E", 1) 774 | x, _ := strconv.ParseFloat(s, 64) 775 | return x 776 | } 777 | disp = "F14.7" 778 | default: 779 | panic("Unsupported TFORM in an Ascii table") 780 | } 781 | 782 | // same as fn function in accessorBin 783 | fn = func(row int) interface{} { 784 | var m sync.Mutex 785 | m.Lock() 786 | if row < 0 || row >= h.Naxis[1] { 787 | return nil 788 | } 789 | b.left = row*h.Naxis[0] + c 790 | x := f() 791 | m.Unlock() 792 | return x 793 | } 794 | 795 | return fn, disp 796 | } 797 | 798 | // verifyPrimary verifies a primary (SIMPLE) header for correctness and the presence of mandatory keys 799 | func (h *Unit) verifyPrimary() error { 800 | _, ok := h.Keys["SIMPLE"] 801 | if !ok { 802 | return fmt.Errorf("No SIMPLE in the primary header") 803 | } 804 | n, ok := h.Keys["BITPIX"].(int) 805 | if !ok { 806 | return fmt.Errorf("No BITPIX in the primary header") 807 | } 808 | if n != 8 && n != 16 && n != 32 && n != 64 && n != -32 && n == -64 { 809 | return fmt.Errorf("Invalid BITPIX value") 810 | } 811 | n, ok = h.Keys["NAXIS"].(int) 812 | if !ok { 813 | return fmt.Errorf("No NAXIS in the primary header") 814 | } 815 | for i := 1; i <= n; i++ { 816 | s := Nth("NAXIS", i) 817 | _, ok := h.Keys[s].(int) 818 | if !ok { 819 | return fmt.Errorf("No %v in the primary header", s) 820 | } 821 | } 822 | return nil 823 | } 824 | 825 | // verifyExtension verifies a secondary (XTENSION) header for correctness and the presence of mandatory keys 826 | func (h *Unit) verifyExtension() error { 827 | xten, ok := h.Keys["XTENSION"].(string) 828 | if !ok { 829 | return fmt.Errorf("No XTENSION in the extended header") 830 | } 831 | n, ok := h.Keys["BITPIX"].(int) 832 | if !ok { 833 | return fmt.Errorf("No BITPIX in the extended header") 834 | } 835 | if n != 8 && n != 16 && n != 32 && n != 64 && n != -32 && n == -64 { 836 | return fmt.Errorf("Invalid BITPIX value") 837 | } 838 | naxis, ok := h.Keys["NAXIS"].(int) 839 | if !ok { 840 | return fmt.Errorf("No NAXIS in the extended header") 841 | } 842 | for i := 1; i <= naxis; i++ { 843 | s := Nth("NAXIS", i) 844 | _, ok := h.Keys[s].(int) 845 | if !ok { 846 | return fmt.Errorf("No %v in the extended header", s) 847 | } 848 | } 849 | pcount, ok := h.Keys["PCOUNT"].(int) 850 | if !ok { 851 | return fmt.Errorf("No PCOUNT in the extended header") 852 | } 853 | _, ok = h.Keys["GCOUNT"].(int) 854 | if !ok { 855 | return fmt.Errorf("No GCOUNT in the extended header") 856 | } 857 | switch xten { 858 | case "IMAGE": 859 | if pcount != 0 { 860 | return fmt.Errorf("PCOUNT should be 0 in IMAGE header") 861 | } 862 | case "TABLE", "BINTABLE": 863 | if n != 8 { 864 | return fmt.Errorf("BITPIX should be 8 in TABLE/BINTABLE headers") 865 | } 866 | if naxis != 2 { 867 | return fmt.Errorf("NAXIS should be 2 in TABLE/BINTABLE headers") 868 | } 869 | } 870 | return nil 871 | } 872 | 873 | // loadTable processes a table (text or binary) data section 874 | // it allocates and reads data 875 | // for each field, it calls accessorBin or accessorText to obtain the corresponding accessor function and adds it to fields 876 | func (h *Unit) loadTable(b *Reader, binary bool) error { 877 | tfields := h.Keys["TFIELDS"].(int) // # of fields 878 | h.list = make([]FieldFunc, tfields) 879 | h.fields = make(map[string]FieldFunc, tfields) 880 | 881 | data := make([]byte, h.Naxis[0]*h.Naxis[1]) 882 | b.Read(data) 883 | h.Data = data 884 | 885 | var col int 886 | for i := 0; i < tfields; i++ { 887 | var fn FieldFunc 888 | var j int 889 | var disp string 890 | form := h.Keys[Nth("TFORM", i+1)].(string) 891 | 892 | if binary { // BINTABLE 893 | j = strings.IndexAny(form, "ABCDEIJKLMPQX") 894 | if j == -1 { 895 | return fmt.Errorf("TFROM has invalid format (binary)") 896 | } 897 | repeat := 1 898 | if j > 0 { 899 | r, _ := strconv.ParseInt(form[:j], 10, 32) 900 | repeat = int(r) 901 | } 902 | if repeat > 0 { 903 | fn, disp = h.accessorBin(form[j], repeat, &col) 904 | } else { 905 | continue 906 | } 907 | } else { // TABLE 908 | j = strings.Index(form, ".") 909 | if j == -1 { 910 | j = len(form) 911 | } 912 | r, _ := strconv.ParseInt(form[1:j], 10, 32) 913 | col = h.Keys[Nth("TBCOL", i+1)].(int) 914 | fn, disp = h.accessorText(form[0], int(r), &col) 915 | } 916 | 917 | h.list[i] = fn 918 | name, ok := h.Keys[Nth("TTYPE", i+1)] 919 | if ok { 920 | h.fields[name.(string)] = fn 921 | h.Keys["#"+name.(string)] = i + 1 // is used to find the index of a field if only its name is given 922 | } else { 923 | h.Keys[Nth("TTYPE", i+1)] = Nth("COL", i+1) // default name given to fields without a corresponding TTYPE 924 | } 925 | 926 | _, ok = h.Keys[Nth("TDISP", i+1)] 927 | if !ok { 928 | h.Keys[Nth("TDISP", i+1)] = disp // if TDISP is missing, the default disp is added to the header as a TDISP 929 | } 930 | } 931 | 932 | return nil 933 | } 934 | 935 | // NewReader generates a new fits.Reader that wraps the given reader 936 | // 2880 is the standard FITS file block size 937 | func NewReader(reader io.Reader) *Reader { 938 | p := new(Reader) 939 | p.buf = make([]byte, 2880) 940 | p.elem = make([]byte, 8) 941 | p.reader = reader 942 | return p 943 | } 944 | 945 | // Read populates p while taking care of the FITS file block structure 946 | func (b *Reader) Read(p []byte) (n int, err error) { 947 | m := len(p) 948 | for { 949 | k := copy(p[n:], b.buf[b.left:b.right]) 950 | n += k 951 | b.left += k 952 | if n == m { 953 | return n, nil 954 | } 955 | b.right, err = b.reader.Read(b.buf) 956 | b.left = 0 957 | if err != nil { 958 | if err == io.EOF { 959 | b.eof = true 960 | return n, nil 961 | } 962 | panic("Error in Reader.Read") 963 | } 964 | } 965 | return 0, fmt.Errorf("unreachable!") 966 | } 967 | 968 | // IsEOF returns if b is finished 969 | func (b *Reader) IsEOF() bool { 970 | return b.eof 971 | } 972 | 973 | // NextPage skips the rest of the current 2880-byte block and reads the next block 974 | func (b *Reader) NextPage() (buf []byte, err error) { 975 | b.right, err = b.reader.Read(b.buf) 976 | b.left = b.right 977 | return b.buf, err 978 | } 979 | 980 | func (b *Reader) ReadByte() byte { 981 | b.Read(b.elem[0:1]) 982 | return b.elem[0] 983 | } 984 | 985 | func (b *Reader) ReadBool() bool { 986 | b.Read(b.elem[0:1]) 987 | return b.elem[0] != 0 988 | } 989 | 990 | func (b *Reader) ReadString(n int) string { 991 | p := make([]byte, n) 992 | b.Read(p) 993 | return string(p) 994 | } 995 | 996 | // ReadInt16 reads an int16 encoded in big-endian binary 997 | // Note that the FITS standard supports only big-endian binaries 998 | func (b *Reader) ReadInt16() int16 { 999 | b.Read(b.elem[0:2]) // we need to copy into an elem buf instead of pointing directly to b.buf because 1000 | // the target value may straddle a block boundary 1001 | x := uint16(b.elem[1]) | uint16(b.elem[0])<<8 1002 | return int16(x) 1003 | } 1004 | 1005 | // ReadInt16 reads an int32 encoded in big-endian binary 1006 | func (b *Reader) ReadInt32() int32 { 1007 | b.Read(b.elem[0:4]) 1008 | x := uint32(b.elem[3]) | uint32(b.elem[2])<<8 | uint32(b.elem[1])<<16 | uint32(b.elem[0])<<24 1009 | return int32(x) 1010 | } 1011 | 1012 | // ReadInt16 reads an int64 encoded in big-endian binary 1013 | func (b *Reader) ReadInt64() int64 { 1014 | b.Read(b.elem[0:8]) 1015 | x := uint64(b.elem[7]) | uint64(b.elem[6])<<8 | uint64(b.elem[5])<<16 | uint64(b.elem[4])<<24 | 1016 | uint64(b.elem[3])<<32 | uint64(b.elem[2])<<40 | uint64(b.elem[1])<<48 | uint64(b.elem[0])<<56 1017 | return int64(x) 1018 | } 1019 | 1020 | // ReadInt16 reads a float32 encoded in big-endian binary 1021 | func (b *Reader) ReadFloat32() float32 { 1022 | b.Read(b.elem[0:4]) 1023 | x := uint32(b.elem[3]) | uint32(b.elem[2])<<8 | uint32(b.elem[1])<<16 | uint32(b.elem[0])<<24 1024 | return math.Float32frombits(x) 1025 | } 1026 | 1027 | // ReadInt16 reads a float64 encoded in big-endian binary 1028 | func (b *Reader) ReadFloat64() float64 { 1029 | b.Read(b.elem[0:8]) 1030 | x := uint64(b.elem[7]) | uint64(b.elem[6])<<8 | uint64(b.elem[5])<<16 | uint64(b.elem[4])<<24 | 1031 | uint64(b.elem[3])<<32 | uint64(b.elem[2])<<40 | uint64(b.elem[1])<<48 | uint64(b.elem[0])<<56 1032 | return math.Float64frombits(x) 1033 | } 1034 | 1035 | // Nth returns a string resulted from concatenation of prefix and n in string form 1036 | // it is a stateless helper function 1037 | func Nth(prefix string, n int) string { 1038 | return fmt.Sprintf("%s%d", prefix, n) 1039 | } 1040 | 1041 | // processString is utilized by NewHeader to process string-type values in the header 1042 | // it uses a 3-state machine to process double single quotes 1043 | func processString(s string) (string, error) { 1044 | var buf bytes.Buffer 1045 | 1046 | state := 0 1047 | for _, char := range s { 1048 | quote := (char == '\'') 1049 | switch state { 1050 | case 0: 1051 | if !quote { 1052 | return "", fmt.Errorf("String does not start with a quote") 1053 | } 1054 | state = 1 1055 | case 1: 1056 | if quote { 1057 | state = 2 1058 | } else { 1059 | buf.WriteRune(char) 1060 | state = 1 1061 | } 1062 | case 2: 1063 | if quote { 1064 | buf.WriteRune(char) 1065 | state = 1 1066 | } else { 1067 | return strings.TrimRight(buf.String(), " "), nil 1068 | } 1069 | } 1070 | } 1071 | return "", fmt.Errorf("String ends prematurely") 1072 | } 1073 | 1074 | // NewHeader reads and processes the next header from the a the reader stream 1075 | // its main function is to populate Keys and setups Naxis 1076 | func (b *Reader) NewHeader() (h *Unit, err error) { 1077 | Keys := make(map[string]interface{}, 50) 1078 | h = &Unit{Keys: Keys} 1079 | 1080 | for { 1081 | buf, err := b.NextPage() 1082 | if err != nil { 1083 | fmt.Println(err) 1084 | return h, err 1085 | } 1086 | 1087 | _lines: 1088 | for i := 0; i < 36; i++ { // each FITS header block is comprised of up to 36 80-byte lines 1089 | s := string(buf[i*80 : (i+1)*80]) 1090 | key := strings.TrimSpace(s[:8]) 1091 | if s[8:10] != "= " { // note that the standard is strict regarding the position of the '=' sign 1092 | Keys[key] = nil 1093 | continue 1094 | } 1095 | 1096 | s = strings.TrimSpace(s[10:]) 1097 | 1098 | if s == "" { 1099 | Keys[key] = nil 1100 | continue 1101 | } 1102 | 1103 | first := rune(s[0]) 1104 | 1105 | if first == '\'' { 1106 | s, err := processString(s) // processes string type values 1107 | if err == nil { 1108 | Keys[key] = s 1109 | } 1110 | continue _lines 1111 | } 1112 | 1113 | j := strings.Index(s, "/") 1114 | if j != -1 { 1115 | s = s[:j] 1116 | } 1117 | 1118 | value := strings.TrimSpace(s) 1119 | 1120 | if value == "" { // we repeat this to take into account for empty values that have comments 1121 | // we could not remove comments before processString because / is valid in a string value 1122 | Keys[key] = nil 1123 | continue 1124 | } 1125 | 1126 | if (first >= '0' && first <= '9') || first == '+' || first == '-' { 1127 | if strings.ContainsAny(value, ".DE") { 1128 | value = strings.Replace(value, "D", "E", 1) // converts D type floats to E type 1129 | x, _ := strconv.ParseFloat(value, 64) 1130 | Keys[key] = x 1131 | } else { 1132 | x, _ := strconv.ParseInt(value, 10, 32) 1133 | Keys[key] = int(x) 1134 | } 1135 | } else if first == 'T' { 1136 | Keys[key] = true 1137 | } else if first == 'F' { 1138 | Keys[key] = false 1139 | } else if first == '(' { 1140 | var x, y float64 1141 | fmt.Sscanf(value, "(%f,%f)", &x, &y) 1142 | Keys[key] = complex(x, y) 1143 | } 1144 | } 1145 | _, ends := Keys["END"] 1146 | if ends { 1147 | item, ok := Keys["NAXIS"] 1148 | if ok { 1149 | n := item.(int) 1150 | h.Naxis = make([]int, n) 1151 | for i := 0; i < n; i++ { 1152 | h.Naxis[i] = Keys[Nth("NAXIS", i+1)].(int) 1153 | } 1154 | } 1155 | 1156 | break 1157 | } 1158 | } 1159 | return h, nil 1160 | } 1161 | 1162 | --------------------------------------------------------------------------------