pax_global_header00006660000000000000000000000064147637762050014533gustar00rootroot0000000000000052 comment=621e5afcd1a7e47d557c37e1f299a8e83c5abd27 nom-tam-fits-1.21.0/000077500000000000000000000000001476377620500141275ustar00rootroot00000000000000nom-tam-fits-1.21.0/.classpath000066400000000000000000000040201476377620500161060ustar00rootroot00000000000000 nom-tam-fits-1.21.0/.github/000077500000000000000000000000001476377620500154675ustar00rootroot00000000000000nom-tam-fits-1.21.0/.github/FUNDING.yml000066400000000000000000000015241476377620500173060ustar00rootroot00000000000000# These are supported funding model platforms github: attipaci patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] nom-tam-fits-1.21.0/.github/badges/000077500000000000000000000000001476377620500167145ustar00rootroot00000000000000nom-tam-fits-1.21.0/.github/badges/jacoco.svg000066400000000000000000000020601476377620500206710ustar00rootroot00000000000000coverage98.8%nom-tam-fits-1.21.0/.github/dependabot.yml000066400000000000000000000010231476377620500203130ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "maven" directory: "/" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" nom-tam-fits-1.21.0/.github/workflows/000077500000000000000000000000001476377620500175245ustar00rootroot00000000000000nom-tam-fits-1.21.0/.github/workflows/build.yml000066400000000000000000000015641476377620500213540ustar00rootroot00000000000000 name: Build on: push: paths: - 'src/main/java/**' - 'src/test/java/**' - 'pom.xml' - '.github/workflows/build.yml' pull_request: paths: - 'src/main/java/**' - 'src/test/java/**' - 'pom.xml' - '.github/workflows/build.yml' jobs: build: name: Compile sources with Maven runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: path: nom-tam-fits - name: Check out blackbox images uses: actions/checkout@v4 with: repository: nom-tam-fits/blackbox-images path: blackbox-images - name: Set up JDK uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' cache: maven - name: Compile run: mvn clean compile working-directory: ./nom-tam-fits nom-tam-fits-1.21.0/.github/workflows/gh-package.yml000066400000000000000000000021631476377620500222400ustar00rootroot00000000000000name: GitHub Package on: release: types: [released] # push: # paths: # - '.github/workflows/gh-package.yml' jobs: publish: runs-on: ubuntu-latest if: ${{ github.repository_owner == 'nom-tam-fits' }} permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' cache: 'maven' - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@v6 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} git_user_signingkey: true - name: Publish to GitHub Packages env: PKG_USERNAME: x-access-token PKG_PASSWORD: ${{ secrets.GITHUB_TOKEN }} run: mvn -B deploy -Dmaven.wagon.httpconnectionManager.ttlSeconds=25 -Dmaven.wagon.http.retryHandler.count=3 -Dmaven.javadoc.skip=true -DskipTests -Pgithub-repo --settings settings.xml nom-tam-fits-1.21.0/.github/workflows/nexus.yml000066400000000000000000000035431476377620500214160ustar00rootroot00000000000000 name: Package on: push: paths: - 'src/main/java/**' - 'src/test/java/**' - 'src/site/**' - 'src/changes/**' - 'pom.xml' - '.github/workflows/nexus.yml' pull_request: paths: - 'src/main/java/**' - 'src/test/java/**' - 'src/site/**' - 'src/changes/**' - 'pom.xml' - '.github/workflows/nexus.yml' jobs: build: name: Build and test package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: path: nom-tam-fits - name: Check out blackbox images uses: actions/checkout@v4 with: repository: nom-tam-fits/blackbox-images path: blackbox-images - name: Set up JDK uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' cache: maven - name: Build package run: mvn clean package -Ppublic-api working-directory: ./nom-tam-fits release: name: Publish to Sonatype needs: build runs-on: ubuntu-latest if: ${{ github.repository_owner == 'nom-tam-fits' && github.ref_name == 'master' }} permissions: contents: read steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' cache: maven - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@v6 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} git_user_signingkey: true - name: Publish package env: NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} run: mvn -B deploy -DskipTests -Pnexus-repo --settings settings.xml nom-tam-fits-1.21.0/.github/workflows/site.yml000066400000000000000000000033041476377620500212130ustar00rootroot00000000000000 name: Project Site on: release: pull_request: paths: - 'src/**' - 'pom.xml' - '*.md' - '.github/workflows/site.yml' push: branches: - master paths: - 'src/site/**' - 'src/changes/**' - 'pom.xml' - '*.md' - '.github/workflows/site.yml' jobs: build: runs-on: ubuntu-latest steps: - name: Checkout master uses: actions/checkout@v4 with: path: nom-tam-fits - name: Checkout gh-pages uses: actions/checkout@v4 with: ref: 'gh-pages' path: site - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' cache: maven server-username: 'git' - name: Build site with Maven run: mvn clean site working-directory: ./nom-tam-fits - name: About this workflow... run: | echo github.event_name = ${{ github.event_name }} echo github.repository_owner = ${{ github.repository_owner }} echo github.ref_name = ${{ github.ref_name }} # Upload site only for official releases or if master commit message contains 'site update' - name: Update site if: github.repository_owner == 'nom-tam-fits' && (github.event_name == 'release' || contains(github.event.head_commit.message, 'site update')) run: | cp -a nom-tam-fits/target/site/* site/ cd site git config --global user.email "$GITHUB_JOB+github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions" git add -A git commit -m "[automated site update]" && git push || true nom-tam-fits-1.21.0/.github/workflows/test.yml000066400000000000000000000036241476377620500212330ustar00rootroot00000000000000 name: Testing on: push: paths: - 'src/main/java/**' - 'src/test/java/**' - 'pom.xml' - '.github/workflows/test.yml' pull_request: paths: - 'src/main/java/**' - 'src/test/java/**' - 'pom.xml' - '.github/workflows/test.yml' jobs: build: name: Build and Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: path: nom-tam-fits - name: Check out blackbox images uses: actions/checkout@v4 with: repository: nom-tam-fits/blackbox-images path: blackbox-images - name: Set up JDK uses: actions/setup-java@v4 with: java-version: '21' distribution: 'adopt' cache: maven - name: Build and run tests run: mvn -B test jacoco:report --file pom.xml working-directory: ./nom-tam-fits - name: Log coverage percentage continue-on-error: true run: | echo "coverage = ${{ steps.jacoco.outputs.coverage }}" echo "branch coverage = ${{ steps.jacoco.outputs.branches }}" - name: Upload test coverage to Codecov.io uses: codecov/codecov-action@v5 continue-on-error: true with: working-directory: ./nom-tam-fits files: ./target/site/jacoco/jacoco.xml fail_ci_if_error: false flags: unittests name: codecov verbose: true token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test coverage to Coveralls.io continue-on-error: true env: COVERALLS_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} run: | if [ "$COVERALLS_TOKEN" != "" ] ; then mvn coveralls:report -DrepoToken=$COVERALLS_TOKEN else echo "WARNING! COVERALLS_REPO_TOKEN secret is undefined. Skipping Coveralls.io upload." fi working-directory: ./nom-tam-fits nom-tam-fits-1.21.0/.gitignore000066400000000000000000000004401476377620500161150ustar00rootroot00000000000000**/*~ **/bin/ target/ pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup pom.xml.next release.properties /LICENSE.txt /jtest.fil /target/ /.metadata/ /RemoteSystemsTempFiles/ /.apt_generated/ /.factorypath /nom-tam-fits.sonargraph/ .idea nom-tam-fits.iml coverage javadoc infer-out nom-tam-fits-1.21.0/.project000066400000000000000000000005631476377620500156020ustar00rootroot00000000000000 nom-tam-fits org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature nom-tam-fits-1.21.0/.settings/000077500000000000000000000000001476377620500160455ustar00rootroot00000000000000nom-tam-fits-1.21.0/.settings/org.eclipse.jdt.core.prefs000066400000000000000000001200571476377620500230340ustar00rootroot00000000000000eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable org.eclipse.jdt.core.compiler.annotation.nullable.secondary= org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.doc.comment.support=enabled org.eclipse.jdt.core.compiler.problem.APILeak=warning org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.autoboxing=ignore org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning org.eclipse.jdt.core.compiler.problem.deadCode=warning org.eclipse.jdt.core.compiler.problem.deprecation=warning org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled org.eclipse.jdt.core.compiler.problem.discouragedReference=warning org.eclipse.jdt.core.compiler.problem.emptyStatement=warning org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled org.eclipse.jdt.core.compiler.problem.fieldHiding=info org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=error org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=protected org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning org.eclipse.jdt.core.compiler.problem.missingJavadocComments=info org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=protected org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag org.eclipse.jdt.core.compiler.problem.missingJavadocTags=warning org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=protected org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=info org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info org.eclipse.jdt.core.compiler.problem.unnecessaryElse=warning org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedImport=warning org.eclipse.jdt.core.compiler.problem.unusedLabel=warning org.eclipse.jdt.core.compiler.problem.unusedLocal=warning org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 org.eclipse.jdt.core.formatter.align_selector_in_method_invocation_on_expression_first_line=false org.eclipse.jdt.core.formatter.align_type_members_on_columns=false org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false org.eclipse.jdt.core.formatter.align_with_spaces=false org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant=0 org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=49 org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=49 org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=49 org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=49 org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=0 org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=49 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 org.eclipse.jdt.core.formatter.alignment_for_assertion_message=0 org.eclipse.jdt.core.formatter.alignment_for_assignment=0 org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 org.eclipse.jdt.core.formatter.alignment_for_compact_if=17 org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=16 org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_arrow=0 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_colon=0 org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_permitted_types_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_record_components=16 org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_switch_case_with_arrow=0 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_type_annotations=0 org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1 org.eclipse.jdt.core.formatter.blank_lines_before_field=0 org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 org.eclipse.jdt.core.formatter.blank_lines_before_method=1 org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 org.eclipse.jdt.core.formatter.blank_lines_before_package=0 org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0 org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_record_constructor=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_record_declaration=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=true org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=true org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false org.eclipse.jdt.core.formatter.comment.format_block_comments=true org.eclipse.jdt.core.formatter.comment.format_header=false org.eclipse.jdt.core.formatter.comment.format_html=true org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true org.eclipse.jdt.core.formatter.comment.format_line_comments=true org.eclipse.jdt.core.formatter.comment.format_source_code=true org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true org.eclipse.jdt.core.formatter.comment.indent_root_tags=false org.eclipse.jdt.core.formatter.comment.indent_tag_description=true org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=insert org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert org.eclipse.jdt.core.formatter.comment.javadoc_do_not_separate_block_tags=false org.eclipse.jdt.core.formatter.comment.line_length=120 org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false org.eclipse.jdt.core.formatter.compact_else_if=true org.eclipse.jdt.core.formatter.continuation_indentation=2 org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true org.eclipse.jdt.core.formatter.indent_empty_lines=false org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false org.eclipse.jdt.core.formatter.indentation.size=4 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_permitted_types=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_permitted_types=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert org.eclipse.jdt.core.formatter.join_lines_in_comments=true org.eclipse.jdt.core.formatter.join_wrapped_lines=true org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false org.eclipse.jdt.core.formatter.keep_switch_body_block_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_switch_case_with_arrow_on_one_line=one_line_never org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never org.eclipse.jdt.core.formatter.lineSplit=124 org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0 org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0 org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0 org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0 org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0 org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true org.eclipse.jdt.core.formatter.tabulation.char=space org.eclipse.jdt.core.formatter.tabulation.size=4 org.eclipse.jdt.core.formatter.text_block_indentation=0 org.eclipse.jdt.core.formatter.use_on_off_tags=false org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator=true org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=false org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true org.eclipse.jdt.core.formatter.wrap_before_switch_case_arrow_operator=false org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter nom-tam-fits-1.21.0/.settings/org.eclipse.jdt.ui.prefs000066400000000000000000000133011476377620500225120ustar00rootroot00000000000000cleanup.add_all=false cleanup.add_default_serial_version_id=true cleanup.add_generated_serial_version_id=false cleanup.add_missing_annotations=true cleanup.add_missing_deprecated_annotations=true cleanup.add_missing_methods=false cleanup.add_missing_nls_tags=false cleanup.add_missing_override_annotations=true cleanup.add_missing_override_annotations_interface_methods=true cleanup.add_serial_version_id=false cleanup.always_use_blocks=true cleanup.always_use_parentheses_in_expressions=false cleanup.always_use_this_for_non_static_field_access=false cleanup.always_use_this_for_non_static_method_access=false cleanup.array_with_curly=false cleanup.arrays_fill=false cleanup.bitwise_conditional_expression=false cleanup.boolean_literal=false cleanup.boolean_value_rather_than_comparison=true cleanup.break_loop=false cleanup.collection_cloning=false cleanup.comparing_on_criteria=false cleanup.comparison_statement=false cleanup.controlflow_merge=true cleanup.convert_functional_interfaces=false cleanup.convert_to_enhanced_for_loop=true cleanup.convert_to_enhanced_for_loop_if_loop_var_used=true cleanup.convert_to_switch_expressions=false cleanup.correct_indentation=true cleanup.do_while_rather_than_while=true cleanup.double_negation=false cleanup.else_if=true cleanup.embedded_if=false cleanup.evaluate_nullable=false cleanup.extract_increment=false cleanup.format_source_code=true cleanup.format_source_code_changes_only=false cleanup.hash=false cleanup.if_condition=true cleanup.insert_inferred_type_arguments=false cleanup.instanceof=false cleanup.instanceof_keyword=false cleanup.invert_equals=false cleanup.join=false cleanup.lazy_logical_operator=false cleanup.make_local_variable_final=true cleanup.make_parameters_final=false cleanup.make_private_fields_final=true cleanup.make_type_abstract_if_missing_method=false cleanup.make_variable_declarations_final=false cleanup.map_cloning=false cleanup.merge_conditional_blocks=true cleanup.multi_catch=false cleanup.never_use_blocks=false cleanup.never_use_parentheses_in_expressions=true cleanup.no_string_creation=false cleanup.no_super=false cleanup.number_suffix=false cleanup.objects_equals=false cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=true cleanup.operand_factorization=false cleanup.organize_imports=true cleanup.overridden_assignment=false cleanup.overridden_assignment_move_decl=true cleanup.plain_replacement=false cleanup.precompile_regex=false cleanup.primitive_comparison=false cleanup.primitive_parsing=false cleanup.primitive_rather_than_wrapper=true cleanup.primitive_serialization=false cleanup.pull_out_if_from_if_else=true cleanup.pull_up_assignment=false cleanup.push_down_negation=false cleanup.qualify_static_field_accesses_with_declaring_class=false cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true cleanup.qualify_static_member_accesses_with_declaring_class=true cleanup.qualify_static_method_accesses_with_declaring_class=false cleanup.reduce_indentation=true cleanup.redundant_comparator=false cleanup.redundant_falling_through_block_end=true cleanup.remove_private_constructors=true cleanup.remove_redundant_modifiers=false cleanup.remove_redundant_semicolons=true cleanup.remove_redundant_type_arguments=true cleanup.remove_trailing_whitespaces=true cleanup.remove_trailing_whitespaces_all=true cleanup.remove_trailing_whitespaces_ignore_empty=false cleanup.remove_unnecessary_array_creation=false cleanup.remove_unnecessary_casts=true cleanup.remove_unnecessary_nls_tags=true cleanup.remove_unused_imports=true cleanup.remove_unused_local_variables=false cleanup.remove_unused_method_parameters=false cleanup.remove_unused_private_fields=true cleanup.remove_unused_private_members=false cleanup.remove_unused_private_methods=true cleanup.remove_unused_private_types=true cleanup.return_expression=false cleanup.simplify_lambda_expression_and_method_ref=false cleanup.single_used_field=false cleanup.sort_members=false cleanup.sort_members_all=false cleanup.standard_comparison=false cleanup.static_inner_class=false cleanup.strictly_equal_or_different=true cleanup.stringbuffer_to_stringbuilder=false cleanup.stringbuilder=false cleanup.stringbuilder_for_local_vars=true cleanup.stringconcat_to_textblock=false cleanup.substring=false cleanup.switch=false cleanup.system_property=false cleanup.system_property_boolean=false cleanup.system_property_file_encoding=false cleanup.system_property_file_separator=false cleanup.system_property_line_separator=false cleanup.system_property_path_separator=false cleanup.ternary_operator=true cleanup.try_with_resource=false cleanup.unlooped_while=false cleanup.unreachable_block=false cleanup.use_anonymous_class_creation=false cleanup.use_autoboxing=false cleanup.use_blocks=true cleanup.use_blocks_only_for_return_and_throw=false cleanup.use_directly_map_method=false cleanup.use_lambda=true cleanup.use_parentheses_in_expressions=false cleanup.use_string_is_blank=false cleanup.use_this_for_non_static_field_access=true cleanup.use_this_for_non_static_field_access_only_if_necessary=true cleanup.use_this_for_non_static_method_access=true cleanup.use_this_for_non_static_method_access_only_if_necessary=true cleanup.use_unboxing=false cleanup.use_var=false cleanup.useless_continue=false cleanup.useless_return=false cleanup.valueof_rather_than_instantiation=false cleanup_profile=_nom-tam-fits cleanup_settings_version=2 eclipse.preferences.version=1 formatter_profile=_nom-tam-fits formatter_settings_version=23 org.eclipse.jdt.ui.ignorelowercasenames=false org.eclipse.jdt.ui.importorder=java;javax;org;com;nom;\#nom; org.eclipse.jdt.ui.ondemandthreshold=99 org.eclipse.jdt.ui.staticondemandthreshold=99 org.eclipse.jdt.ui.text.custom_code_templates= nom-tam-fits-1.21.0/BUILD.md000066400000000000000000000042501476377620500153110ustar00rootroot00000000000000# Building your own nom-tam-fits Starting with version 1.14 the FITS library uses a number of features that make Maven the recommended method for building the FITS library. These instructions are provided for a Unix/Linux but can be easily adapted for other platforms (such as Windows or MacOS X) also (Architecture independence is one reason using tools like Maven is nice!) 1. Grab the source from [github.com/nom-tam-fits/nom-tam-fits](https://github.com/nom.tam.fits/nom.tam.fits) You can simply clone the repository, e.g. ```bash git clone https://github.com/nom-tam-fits/nom-tam-fits.git ``` from your preferred location on your machine. After cloning you can of course set your local repository to any given earlier commit as well, if you want to work with a particular version of the source code. Alternatively, you can check out [Releases](https://github.com/nom-tam-fits/nom-tam-fits/releases) and grab the source tarball from your preferred release, and extract it at the preferred location: ```bash tar xzf nom-tam-fits-1.xx.x.tar.gz ``` 2. Next, you will need __maven__ installed on your system as well as a JDK (such as __openjdk__) version 9 or later. If you are using an RPM-based Linux distro, you can install maven via __dnf__ / __yum__ as: ```bash sudo dnf install maven java-latest-openjdk ``` Equivalently, for Debian-based distros: ```bash sudo apt install maven default-jdk ``` 3. Now build the package using maven: ```bash cd nom-tam-fits mvn package ``` This will check and compile the source, create the jars, runs the tests suite, builds the javadocs, checks for bugs, and builds the web-site locally. The JARs can be found under the `target/` directory (sorry, there is also a lot of junk there from the tests_. The full API documentation can be found under `target/apidocs`, while the more restricted set of Public User API documentation is under `target/site/apidocs`. The local web-site including the Getting Started guide, unit test coverage information, changelog, etc are under `target/site` (you can simply open `index.html` in your browser to see). nom-tam-fits-1.21.0/CHANGELOG.md000066400000000000000000002363051476377620500157510ustar00rootroot00000000000000# Changelog All notable changes to the nom.tam.fits library will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.21.0] - 2025-03-11 Feature release, with a few bug fixes. ### Fixed - [#708] `BinaryTable.addRowEntries()` did not work with scalar `Boolean` or `boolean` values. (by @attipaci, thanks to @johnmurphyastro) - [#710] `BinaryTable.addRow()` has some oddities and inconsistencies that resulted in unexpected behavior. (by @attipaci) - [#730] Decompression errors when reading compressed images that used compression quantization (`ZSCALE`, `ZZERO`, `ZBLANK`, and `ZQUANTIZ` options) together with a lossless compression algorithm (e.g. GZIP or PLIO). (by @attipaci, thanks to @bogdanni) ### Added - [#717] New `RandomAccessFileChannel` class, which wraps `FileChannel` objects to provide `RandomAccessFileIO` for FITS files. For example, this could be used in conjunction with the Amazon Java SDK (https://aws.amazon.com/sdk-for-java/) to connect to an S3 instance. (by @at88mph) ### Changed - [#722] Synchronization fixes for issues detected by latest spotbugs. These should improve thread safety, especially of binary tables and compressed tables. (by @attipaci) - [#726] Bump to Java 21 for CI builds. (by @attipaci) - [#737] Remove spotbug supressions that are no longer needed. (@attipaci) - Uses the latest maven build and runtime dependencies. (by @attipaci) ## [1.20.2] - 2024-12-01 Minor bug-fix release. ### Changed - [#696] Flexible space around `=` for HIERARCH cards, allowing slightly longer inline values (by @attipaci, thanks to @vforchi). - [#697] Tweaks to how space available is calculated for cards, allowing slightly longer (hierarch) keys or values, depending on value types. The calculations are also more consistent across the various methods that use them. (by @attipaci) - Uses the latest maven build and runtime dependencies. (by @attipaci) ## [1.20.1] - 2024-09-01 Bug fix release. ### Fixed - [#636] Compressed table columns lost their column names in 1.20.0. (by @attipaci, thanks to @keastrid) - [#656] Fixed incorrect value types in several FITS keywords extension dictionaries. (by @attipaci, thanks to @johnmurphyastro) - [#664] Recalculate stored checksums when writing headers to avoid checksums being set prematurely by `setChecksum()` methods. (by @attipaci, thanks to @johnmurphyastro) ### Added - [#657] Added `ESOExt` enum with ESO-specific standard FITS keywords, and `CommonExt` for commonly used keywords in the amateur astronomy community. (by @attipaci) - [#657] Added more standard keywords to `CXCEXt`, `STScIExt`, `MaxImDLExt`, and `SBFitsExt` dictionaries based on the available documentation online. (by @attipaci) - [#657] Additional keyword synonyms in `Synonyms` enum. (by @attipaci) ### Changed - [#657] Updated comment fields and Javadoc for many extension keywords in the `nom.tam.fits.header.extra` package. (by @attipaci) - [#665] Keep original comments for mandatory keywords when writing headers that were previously read from an input. (by @attipaci) - [#665] `CHECKSUM` and `DATASUM` comments now store timestamp when checksums were calculated, as is the recommended practice by the checksumming standard. (by @attipaci) - [#672] Switched to markdown [keep a changelog](https://keepachangelog.com/en/1.1.0/). - Do not generate reports that have very little use. (by @attipaci) - Uses the latest maven build and runtime dependencies. (by @attipaci) ### Deprecated - [#657] Deprecated the `CXCStclShareExt` enum. Not only its name was completely botched, its definitions are now included properly in the `CXCExt` and `STScIExt` dictionaries (without reduntant definitions). (by @attipaci) - [#657] Deprecated a few extension keywords, which are either part of the standard or are recognised by HEASARC as commonly used keywords. The mentioned more common definitions should be used instead going forward. (by @attipaci) - [#665] Deprecated some checksuming methods that are no longer useful, as they do not offer a mean to verify FITS files. Users might want to use `BasicHDU.verifyIntegrity()` or `Fits.verifyIntegrity()` instead. (by @attipaci) ## [1.20.0] - 2024-05-10 Image quantization, slicing and sampling, and more. ### Fixed - [#596] Botched compression of multicolumn tables, resulting in extra rows in the compressed table (as many rows as there were total table tiles across all columns). (by @attipaci, thanks to @keastrid) - [#610] `Standard.TNULLn` keyword can take any argument type (was previously restricted to strings only). (by @attipaci) - [#621] `MultiArrayIterator` fixes to `size()` and `deepComponentType()` methods. (by @attipaci) ### Added - [#586] Default column names. Some tools, including (C)FITSIO have issues dealing with table columns that have no `CTYPEn` keywords set to define column names. Therefore to enhance interoperability we will use 'Column N' type naming, until the user sets a column name otherwise. (by @attipaci) - [#611] New `Quantizer` class to handle generic decimal-integer conversions in line with the FITS specification for integer representation of floating-point data. The new class will interpret the necessary header keywords for both images and tables, and can fill the required values in FITS headers. (by @attipaci) - [#611] `ImageData.setQuantizer()` / `.getQuantizer()` and `BinaryTable.ColumnDesc.setQuantizer()` / `.getQuantizer()` methods to access the quantizers that handle decimal-integer conversions for images and binary table columns, respectively. (by @attipaci) - [#610] `ImageData.convertTo(Class type)` will return the equivalent image data with another storage type (but otherwise the same as the original image). Any decimal-integer conversion is handled by the image's own quantizer (which is either specified in the image header or else set by the user) if it is available. (by @attipaci) - [#610] New `BinaryTable.getArrayElement(row, col)` and `.getArrayElementAs(int row, int col, Class asType)` methods to return the table element as an array always (without the boxing of scalar entries). The former is similar to the original `.getElement()` method except that it represents FITS logicals as `Boolean[]...`, bits as `boolean[]...`, and complex values a `ComplexValue[]...` arrays. The second form allows accessing numerical table elements as arrays of a desired other numerical type. (by @attipaci) - [#610] `ArrayFuncs.convertArray(Object array, Class newType, Quantizer quant)` method provides generic conversions to numerical type arrays, including `ComplexValue` type arrays, and can be used for explicit quantization conversions outside of images and table columns. (by @attipaci) - [#592] `ImageData` now supports complex valued images as per the FITS standard, both when constructed programatically from `ComplexValue[]...` data; or if so described with an image axis whose `CTYPEn` header value is 'COMPLEX' and the axis contains 2 components (so long as the complex axis is either the first or last axis). The latter is true even if integer representation is used in the FITS, e.g. via a quantization. When reading complex valued images, the native `ImageData` is that of the stored type (e.g. `float[]..[2]` or `float[2]..[]`) for compatibility, but `ImageData.convertTo(ComplexValue.class)` or `.convertTo(ComplexValue.Float.class)` can convert these to be true complex valued images in a second step after reading. (You may also use `ImageData.isComplexValued()` to check if the data were designated as complex-valued at origination, by the FITS header or the constructor.) (by @attipaci) - [#619] New `ArrayFuncs.sample()` and `.slice()` methods, to provide generic support for obtaining slices and samplings from arrays in one or more dimensions, with the option of reversed order sampling along selected axes. (by @attipaci, thanks to @at88mph) ### Changed - [#590] Removed `Main` class, and `main()` entry points from JAR, which also clears up a whole lot of junk being added to the classpath into `MANIFEST.MF`. (by @attipaci, thanks to @bogdanni) - [#592] `ArrayFuncs.convertArray(Object, Class)` is now the same as the new `ArrayFuncs.convertArray(Object, Class, Quantizer)` method without a quantizer, and thus allows conversions to/from complex valued data also, when primitive arrays have a trailing dimension of 2 (e.g. `float[]..[2]` or `int[]..[2]`). (by @attipaci) - [#609] `MultiArrayCopier.copyInto()` now rounds floating point values before casting to integer types for a more regular behavior. (by @attipaci) - [#611] `BinaryTable.getDouble(...)` and `.getLong(...)` and `.set(...)` methods will now use the specified quantization (as specified in the header or else set explicitly by the user) when converting from decimal to integer types and vice-versa. (by @attipaci) - [#620] Updated github.io landing page with history and partial listing of software that uses nom-tam-fits. (by @attipaci) - [#620] Fix up POM to cure various build warnings, and allow Google Analytics for online Javadoc. (by @attipaci) - [#623] Fix up test code to use latest `nanohttpd-webserver` for unit testing. (by @attipaci) - Updated User's Guide (`README.md`). (by @attipaci) - Uses the latest maven build and runtime dependencies. (by @attipaci) ## [1.19.1] - 2024-03-25 Bug fix release, with improved table compression support. ### Fixed - [#562] Compressed tables had the `ZCTYPn` keywords written with a 0-based index, and thus were off-by-one in the FITS convention. Now fixed. (by @attipaci, thanks to @keastrid) - [#552] Fix inconsistently maintained `ColumnDesc.fileSize`, by replacing it with dynamic `.rowLen()`. (by @attipaci, thanks to @keastrid) - [#557] Fix the compression of variable-length table columns. These were completely off-spec in prior releases, but are now properly supported as per the (C)FITSIO convention, which is slightly deviating from the documented standard. However, based on private communication with the maintainers, they are inclined to resolve this discrepancy by updating the standard to match the (C)FITSIO implementation, and therefore we adopt the (C)FITSIO way also for compressing tables. Nevertheless, we provide the static `CompressedTableHDU.useOldStandardIndexing(boolean)` method to allow reading compressed VLA columns produced in the other way also (i.e as the current standard actually describes it). Note, that our VLA compression support is now much better than that of (C)FITSIO, whose current release contains multiple severe bugs in that regard. (by @attipaci, thanks to @keastrid) - [#570] Fix potential narrowing conversion issues uncovered by GitHub's CodeQL. (by @attipaci) - [#576] Eliminate unexpected exception thrown if trying to write an empty table to a file/stream. Now it works as expected. (by @attipaci) - [#578] Check setting elements in `ColumnTable` more consistently to avoid creating corrupted FITS tables. (by @attipaci) - [#580] Fix `ArrayIndexOutOfBoundsException` when defragmenting larger heaps. (by @attipaci, thanks to @keastrid) ### Added - [#558] Find header cards with regex pattern matching with `Header.findCards()`. (by @rmathar) - [#563] Added support for optionless Rice and HCOMPRESS compression, using the default option values as per the FITS standard. As a result `RICE_1` can now be used for compressing integer-type binary table columns. (by @attipaci) - [#575] Added support for reserving space in FITS files for future additions to binary tables in situ without disturbing the rest of the FITS file. Space for extra table rows can be reserved via `BinaryTable.reserveRowSpace(int)`, while additional heap space for VLA additions / updates can be set aside by `BinaryTable.reserveHeapSpace(int)` prior to writing the table to a file. Note, however that (C)FITSIO may not be able to read files with reserved row space due to its lack of support for the standard `THEAP` keyword. (by @attipaci) ### Changed - [#563] Creating compressed tables now also checks that the requested compression algorithms are in fact suitable for table compression, or else throws an `IllegalArgumentException`. (Currently, only the lossless `GZIP_1`, `GZIP_2`, `RICE_1`, and `NOCOMPRESS` are allowed by the FITS standard for tables.) (by @attipaci) - [#567] The parallel table compression has previously resulted in a heap with irreproducible random ordering. Now, the compressed heap is ordered consistently for optimal sequential read performance. (by @attipaci) - [#571] From here on we'll omit writing the `THEAP` keyword into binary table headers when they are not necessary (that is when the heap follows immediately after the main table). This allows better interoperability with (C)FITSIO, which currently lacks proper support for this standard FITS keyword. (by @attipaci) - [#582] Support heaps up to 2GB (previously they were limited to 1GB max). (by @attipaci) - Uses the latest maven build and runtime dependencies. (by @attipaci) ## [1.19.0] - 2024-01-19 Feature release with bug fixes. ### Fixed - [#496] Workaround for read-only FITS files on Windows network shares. (by @cek) - [#531] Keywords with hyphens in `STScIExt` had the wrong form previously. (by @attipaci) - [#532] Small fixes to `HashedList` handling iterator access corner cases. (by @attipaci) - [#535] Binary table `NAXIS1` value was sometimes stored as a string. (by @attipaci) ### Added - [#488] New targeted `Data` and HDU creation from Java objects via new `Data+.from(Object)` and `Data+.toHDU()` methods, replacing the now deprecated and unsafe `[...]HDU.encapsulate(Object)` methods. (by @attipaci) - [#489] New `Header.mergeDistinct(Header)` method to allow copying non-conflicting header keywords from one FITS header to another. (by @attipaci, thanks to @robyww) - [#490] New inspection methods to `UndefinedData` class, to easily retrieve `XTENSION` type, `BITPIX`, and data size/shape information from HDUs containing unsupported data types. (by @attipaci) - [#492] New access methods for parameters stored in `RandomGroupsHDU` types, according to the FITS specification. (by @attipaci) - [#494] A better way to control how FITS `I10` type columns are treated in ASCII tables, via static `AsciiTable.setI10PreferInt(boolean)` and `.isI10PreferInt()` methods. (by @attipaci) - [#520] New methods to make the decompression of selected `CompressedTableHDU` tiles even easier (and safer) to use. (by @attipaci) - [#531] New `WCS` and `DateTime` enums with an enumeration of all standard FITS WCS and date-time related keywords, and recognized values (constants). The `WCS` enums support alternate coordinate systems via the `WCS.alt(char)` method. For example to generate the "CTYPE1A" standard keyword you may call `WCS.CTYPEna.alt('A').n(1)`. (by @attipaci) - [#532] Support for `INHERIT` keyword via `Fits.getCompleteHeader(...)` methods. These methods will populate the mandatory FITS header keywords in the the selected HDU, and return either the updated header or else a new header composed up of both the explicitly defined keywords in the selected HDU and the inherited (non-conflicting) entries from the primary HDU. (by @attipaci) - [#534] Adding standardized (`IFitsHeader`) keywords to HDU headers will now check that the keyword can be used with the associated HDU type (if any), and may throw an `IllegalArgumentException` if the usage is inappropriate. The type of checks (if any) can be adjusted via the new Header.setKeywordChecking() and/or the static `Header.setDefaultKeywordChecking()` methods. (by @attipaci) - [#534] Setting values for `HeaderCards` with standardized (`IFitsHeader`) keywords now checks that the assigned value is compatible for the given keyword, and may throw an appropriate exception (such as a newly added `ValueTypeException`), or else log a warning message depending on the current value checking policy. The policy can be adjusted via the new static `HeaderCard.setValueCheckPolicy()` method. (by @attipaci) - [#538] New `FitsKey` class to replace the poorly named `FitsHeaderImpl` class. (The latter continues to exist in deprecated form for compatibility). Added new functionality. (by @attipaci) - [#538] New `Standard.match(String)` method to match one of the reserved keyword templates of the FITS standard to a keyword instance, such as "CTYPE1A" to `WCS.CTYPEna`. (by @attipaci) ### Changed - [#519] Demoted `FitsException` to be a soft exception, extending `IllegalStateException`. As a result, users are no longer required to catch these, or its derivatives such as `HeaderCardException`. Some methods that have previously thrown `IllegalStateException` may now throw a `FitsException`, in a backwards compatible way. (by @attipaci) - [#518] The constructor of `PaddingException` does not throw a `FitsException`, and `FitsCheckSum.Checksum` also does not throw an `IllegalArgumentException` -- so they are now declared as such. Some related Javadoc updates. (by @attipaci) - [#525] Improvements to `AsciiTable` with `addColumn()` argument checking and more specific documentation. (by @attipaci) - [#531] `Synonyms` has been extended to enumerate all synonymous WCS keys and a few other keywords with equivalent definitions. (by @attipaci) - [#531] Added `impl()` method to `IFitsHeader` interface with a default implementation to eliminate code that was duplicated many times over across enums. (by @attipaci) - [#531] `IFitsHeader.n(int...)` now checks that the supplied indices are in the 0-999 range, or else throw an `IndexOutOfBoundsException`. If too many indices are supplied it will throw a `NoSuchElementException`. And, if the resulting indexed keyword exceeds the 8-byte limit of standard FITS keywords it will throw an `IllegalStateException`. (by @attipaci) - [#531] Attempting to create a `HeaderCard` with a standard keyword that has unfilled indices will throw an `IllegalArgumentException`. (by @attipaci) - `CompressedImageHDU.getTileHDU()` now updates `CRPIXna` keywords also in the alternate coordinate systems (if present) for the selected cutout image. (by @attipaci) - [#538] Corrections to standard keyword sources, deprecations, synonyms, comments and associated javadoc. (by @attipaci) - Change from unmaintained `findbugs` build dep to `spotbugs` successor. (by @attipaci) - Migrate testing from JUnit 4 to JUnit 5 Vintage. (by @attipaci) - Uses the latest maven build and runtime dependencies (by @attipaci) - Fully revised `README` (User's guide), with better organization, new sections, further clarifications, and many corrections. (by @attipaci) ### Deprecated - [#519] Deprecated `IHeaderAccess` interface and its implementations. These were meant for internal use anyway, and with `HeaderCardException` now being a soft exception these classes just add confusion without providing any functionality that isn't readily available without them. (by @attipaci) ## [1.18.1] - 2023-09-11 Important bug fix release with minor enhancements. ### Fixed - [#466] Fixed broken default reading of ASCII tables in 1.18.0, without explicitly having to set `FitsFactory.setUseAsciiTables(true)`. (by @attipaci, thanks to @bogdanni) ### Added - [#474] New `Fits.verifyIntegrity()`, `BasicHDU.verifyIntegrity()` and `.verifyDataIntegrity()` methods for verifying file / HDU integrity based on the stored checksums and/or data sums. The now useless checksum access methods of before have been deprecated and should be avoided. (by @attipaci) - [#476] `FitsInputStream` to calculate checksums for all bytes that are read/skipped, allowing checksum verification for stream-based inputs also. (by @attipaci) - [#475] Added `Header.getCard()` methods similar to existing `.findCard()`, but without changing the position at which new cards are added. The new method is now used more widely internally when interpreting headers. New `Header.prevCard()` method to complement existing `.nextCard()`, and `.seekHead()` / `.seekTail()` methods to set the position for new additions at the head or tail of the header space respectively. (by @attipaci) ### Changed - [#473] Updated `README` for proper checksum verification procedures. (by @attipaci) - [#472] New cleaner-looking logos for github icons / profile image. (by @attipaci, thanks to @tony-johnson and @ritchieGithub) - Updated maven build and runtime dependencies to their latest releases. (by @attipaci) ## [1.18.0] - 2023-07-24 Feature release with compression fixes and many improvements. ### Fixed - [#377] Fixed use of `ZDITHER0` keyword to restore or record random generator seed when dithering is used in (de)compression. (by @attipaci) - [#349] Fixed tiled (de)compresssion of some quantized images via GZIP. (by @attipaci, @thanks to @bogdanni and @keastrid) - [#436] Fixed incorrect handling of `CONTINUE` keywords with no quoted string value (a regression since 1.16). (by @attipaci) - [#438] Conversion of variable length `float[]` or `double[]` arrays to complex was broken since forever. Now it works for the first time as expected. (by @attipaci) - [#455] Fixed incorrect variable-length complex array descriptors in binary tables. (by @attipaci) - [#445] Consistent String return for both fixed and variable-length string columns in binary tables. (by @attipaci) - [#456] Fixed compressed table tiling with tile sizes that do not divide the table rows. (by @attipaci) - [#460] Fixed incorrect `HCompress` `SCALE` parameter handling. (by @attipaci, thanks to @keastrid) ### Added - [#387] Added ability to stride while tiling. (by @at88mph) - [#356] Added ability to stream image cutouts. (by @at88mph) - [#400] Added ability to stream compressed image cutouts. (by @at88mph) - [#335] Added `NullDataHDU` for more intuitive support for header only HDUs with no associated data. (by @attipaci) - [#352] Added support for alternative `RandomAccess` immplementations. (by @at88mph) - [#183] Added support for `ComplexValue`-based column in `BinaryTable` (by @attipaci) - [#450] Added support for bit-based columns (FITS type `X`) in binary tables. (by @attipaci) - [#451] Added support for `null` (undefined) logical values for FITS logical columns in binary tables. (by @attipaci) - [#448] More user-friendly data access in `BinaryTable`, using Java boxing for scalar primitive entries, and automatic type conversion (both narrowing and widening) for singleton values when possible. Many new `BinaryTable` methods to make it easier to build and use binary tables. (by @attipaci) - [#437] Allow accessing binary table entries / rows directly from file in deferred read mode. The `README` for reading binary tables has been updated accordingly. (by @attipaci) - [#453] Allow defragmenting binary table heaps to expunge stale data, and to optimize heap area for best sequential read performance. (by @attipaci) - [#452] Simplified access to select tiles / regions of compressed data in both tile compressed tables and images. (by @attipaci) - [#178] Support for the Substring Array Convention. (However, we will prefer the use of the more generic `TDIMn` over the convention for fixed-width string-based columns.) (by @attipaci) - [#458] Use `HIERARCH`-style keywords more easily within the library via the static methods of the Hierarch class. (by @attipaci) ### Changed - [#414] Significantly improved IO performance, now typically 2.5 to 4 times faster for images than prior releases and a whopping 25-50 times faster for reading streams than 1.15.2. (by @attipaci) - [#407] Improved dithering performance by calculating the random sequence only once, and more efficiently. (by @attipaci, thanks to @keastrid) - [#441] Significantly faster table data access and row additions. (by @attipaci) - [#207] Unified deferred read implementation for all `Data` classes. (by @attipaci) - [#395] Consistent behavior and method visibilities across `Data` subclasses. (by @attipaci) - [#434] Stop the automatic creation of random group HDUs from compatible table data. Instead create binary or ASCII tables as appropriate given the `FitsFactory` settings. The FITS standard discourages the use of random group HDUs going forward, and support should be typically limited to dealing with some older existing data files in that format. If there is ever a need to create new random group HDUs, you can do it explicitly via `RandomGroupsHDU.createFrom(Object[][])`. (by @attipaci) - [#446] Disable automatic (and inconsistent) detection of complex binary table data, when adding real-valued data columns to a binary table. Users should call `BinaryTable.setComplexColumn()` explicitly if pairwise real values should be converted to complex. (by @attipaci) - [#447] Check data consistency when adding or setting data in binary tables. (by @attipaci) - [#454] Do not use ASCII `NUL` (`0x00`) for padding between string array elements in binary tables, since it violates the FITS standard, in which an ASCII `NUL` terminates the entire string array entry. (by @attipaci) - [#131] Tables now support up to `Integer.MAX_VALUE` (~2-billion) rows regardless of entry size. (by @attipaci) - [#461] Improved handling of shared compression settings across option copies, and including validation (by @attipaci) - [#394] Various tweaks to `FitsDate` that should not affect user behavior. (by @attipaci) - [#420] Revised `PaddingException` API and seamless internal handling. (by @attipaci) - [#435] Header access methods in `BasicHDU` no longer remove significant leading spaces from strings. (by @attipaci) - [#426] No longer exposing mutable internal arrays to users, giving them copies instead. (by @attipaci) - [#429] Remove unnecessary IO synchronization in `FitsFile`, `FitsInputStream`, and `FitsOutputStream` classes. (by @attipaci) - [#424] Code cleanup -- Consistent source code formating etc. (by @attipaci) - [#160] Public user API javadoc now complete. It could be improved, but at least all packages, classes, and methods intentionally exposed to users are documented. (by @attipaci) - [#427] Online API documentation is restricted to public classes and methods only, as appropriate for users who want to read/write FITS files. Classes and methods intended for internal use are clearly indicated when these are exposed in the public. Contributors can still generate the full documentation locally if desired using maven. (by @attipaci) - Fully revised `README` (Getting Started guide) with better and more up-to-date examples. (by @attipaci) - [#402] Upgrade from JDK 11 to 17 for creating signed packages in the CI (but still in Java 8 compatibility mode). (by @attipaci) - Updated maven build and runtime dependencies to their latest releases. (by @attipaci) ### Deprecated - [#336] Deprecate unsafe methods that should only be used internally (if at all) and never by users. (by @attipaci) - [#209] `FitsUtil.reposition()` deprecated and changed to take `FitsIO` argument to match usage. (by @attipaci) - [#425] Deprecate hexadecimal values in headers. FITS does not support these, and you should not use them. (by @attipaci) - [#430] Deprecate `BigInteger` and `BigDecimal` values in headers. FITS does not support these, and you should not use them. (by @attipaci) ## [1.17.1] - 2023-03-15 Maintenance release with critical bug fixes. ### Fixed - [#368] Fixed first extension written as primary if primary HDU contained no data (affects 1.17.0). (by @attipaci, thanks to @Pharisaeus) - [#367] Fixed incorrect checksum calculated directly from file in multi-HDU FITS, and other checksum fixes (affects 1.17.0). (by @attipaci, thanks to @Pharisaeus) - [#341] Fixed occasional `NullPointerException` in tiled image compression resulting from incomplete initializiation of `TiledImageCompressionOperation`. (by @keastrid) ### Added - [#376] Upload coverage report to Coveralls.io also (forks need to set a repo token via the repo secret `COVERALLS_REPO_TOKEN` if want to enable reporting also). (by @attipaci) ### Changed - [#373] Fixed GitHub Actions site build error, by removing unused broken dependency from POM. (by @attipaci) ## [1.17.0] - 2022-09-11 Improved image compression, checksum support, and incremental writing ### Fixed - [#318] Fixed broken tile compression of non-square images. (by @attipaci, thanks to @keastrid and @BCowan12) - [#318] `CompressedImageHDU.fromInageHDU()` now defaults to tiling by row if tile size is not explicitly specified, as per FITS specification and also for consistent behavior in higher dimensions. (by @attipaci) ### Added - [#319] Generalized tile compression for any-dimensional images based on the FITSIO convention. (by @attipaci, thanks to @ritchieGithub) - [#323] New checksumming methods: `BasicHDU.calcChecksum()`, `.setChecksum()`, `.getStoredChecksum()`, and `.getStoredDataSum()`, `Data.calcChecksum()`, `Fits.calcChecksum(int)` and `.setChecksum(int)` -- as well as an extended static API for `FitsCheckSum`. (by @attipaci) - [#323] Added `Fits.rewrite()` to simplify re-writing the entire Fits objects, e.g. after updating checksums. The implementation is efficient in that it skips data segments in deferred read mode. `BasicHDU.rewrite()` is modified to make it efficient also. (by @attipaci) - [#283] User adjustable header comment alignment position via `Header.setCommentAlignPosition(int)` and checking via `.getCommentAlignPosition()`. (by @attipaci) - [#145] New `Fits.getHDU()` methods to select HDU from Fits by name (and version). (by @attipaci, thanks to @at88mph) - [#323] Added `Data.isDeferred()` method (defaulting to false) that can be used to check if a FITS Data object might be in deferred read mode, that is if it may not be fully loaded into RAM. Note, that while the method is properly implemented for the built-in data types of the library, it may not properly reflect the deferred status of external data implementations unless these override the method with something meaningful also. (by @attipaci) ### Changed - [#266] Safe incremental HDU writing via new `FitsOutput` interface, which is used to check whether HDUs should be set primary or not depending on where it is located in the output. (by @attipaci, thanks to @keastrid) - [#323] Simpler, faster, and more versatile `FitsChecksum` class, with support for incremental checksum updates, checksum retrieval, and checksum computations directly from files. (by @attipaci) - [#328] Checksum in-memory headers/data with less overhead through piped streams. (by @attipaci) - [#323] `Fits.setChecksum()`, `.setCheckSum(int)`, and `.calcChecksum(int)` compute checksum directly from the file, if possible, for deferred read data (i.e. data not currently loaded into RAM). This eliminates the need to keep potentially huge data volumes in RAM when computing or updating checksums in an existing FITS file. (by @attipaci) - [#323] `Fits.setChecksum()` will now checksum all HDUs, including those not already loaded from disk -- keeping data in deferred read mode if possible. (by @attipaci) - [#311] Duplicate header keys, during parsing, are repoted though separate logger instance from Header's, with verbosity controlled via `Header.setParserWarningsEnabled(boolean)`. (by @attipaci) - [#292] Suppress repeated duplicate keyword warnings when parsing and improve checks for FITS standard violations. Added `Header.getDuplicateKeySet()` method to check which keywords have duplicates. (by @attipaci) - [#292] Replacing header keys logs a warning when existing value type is incompatible wth the newly associated standardized keyword. (by @attipaci) - [#292] Creation of header entries with standardized keywords logs a warning if associated value type is incompatible with the keyword. (by @attipaci) - [#292] Improved header card ordering implementation. (by @attipaci) - [#74] New logo. (by @attipaci) ## [1.16.1] - 2022-03-21 Maintenance release with bug fixes. ### Fixed - [#252] Fixed broken Java 8 compatibility of 1.16.0 due to Java compiler flags in POM. Note, after the change the build itself will require Java 9 or later! (by @attipaci, thanks to @harmanea) - [#243] Fixed potential unchecked null in `BinaryTableHDU`. (by @attipaci) ### Changed - [#257] No `Logger` warnings about duplicate header cards if `Header` parser warnings are disabled. (by @attipaci, thanks to @harmanea) - [#210] Added default `ArrayDataInput`/`ArrayDataOutput` implementations. (by @attipaci) - [#229] Removed runtime dependency on `javax.annotation-api`. (by @attipaci, thanks to @bogdanni) - Mostly automated updates of dependencies. (by @dependabot) ## [1.16.0] - 2021-12-13 Compliance to FITS 4.0 standard, plus many more fixes and improvements. ### Fixed - [#171] Prevent the creation of invalid header entries from code, by throwing informative runtime exceptions. New runtime exception classes `HierarchNotEnabledException`, `LongStringsNotEnabledException`, `LongValueException`, `UnclosedQuoteException` are used to report when illegal action was pre-empted relating to FITS headers. (by @attipaci) - [#165] Prevent creating header cards with `NaN` and `Infinite` values. The FITS standard does not support these. (by @attipaci) - [#187] No `EOFException` is thrown when skipping beyond the end of file, since it should be allowed in random access mode. (by @attipaci) - [#186] Consistent handling of logical (`true`/`false`) values in FITS binary tables, including newly added support for `null` (or undefined) values also as per FITS standard. (by @attipaci) - [#184] In prior versions `char[]` arrays in binary tables were written as 16-bit Unicode and read back as `short[]` integers. FITS recognises only ASCII character arrays with 1-byte per character. A new `FitsFactory.setUseUnicodeChars(boolean)` option can toggle compliance to the FITS standard for `char[]` arrays. However, the misconceived prior behavior remains the default to maintain back compatibility until the next major release. (by @attipaci) - [#153] No more `Logger` warnings on multiple `CONTINUE` keywords, tolerant `HIERARCH` parsing, and other small fixes. (by @attipaci) - [#135] Fix management of sub-seconds in `FitsDate` (by @Zlika) - [#130] Check for and reject non-ASCII or non-printable characters in headers. The FITS standard allows only ASCII characters in the range of `0x20` to `0x7E` in the headers. The new static method `HeaderCard.sanitize(String)` is available to users to replace characters outside of the supported range with `?`. (by @olebole) - [#123] Minor fixes prior to release (by @wcleveland) - [#162] Revised when exceptions are thrown, and they are made more informative by providing more essential details and traceable causes. (by @attipaci) - [#159] `HIERARCH` header cards are now written to conform to 'cfitsio' specification, which requires a space before '='. While the `HIERARCH` convention itself does not specify the extra space, it certainly allows for it, and with the change our FITS files shall be more conformant to, and readable, by yet another widely used library. (by @attipaci) - [#158] Check for `markSupported()` when attempting to use `mark()` or `reset()` methods in `ArrayDataInput`, and throw an appropriate runtime exception if the methods are not supported by the implementation. (by @mbtaylor, thanks to @olebole) - [#156] Fixed issues with handling of single quotes as part of user-supplied strings. (by @attipaci, thanks to @keastrid) - [#143] `I10` format ASCII tables are parsed as 32-bit `int[]` by default (for back compatibility), unless `TLMIN`/`TLMAX` or `TDMIN`/`TDMAX` header entries indicate a more extended range. Added new `AsciiTable(Header, boolean)` constructor to optionally change the preference to read `I10` ASCII table data as 64-bit `long[]` columns. (by @mbtaylor) - [#190] Changed to generated `serialVersionUIDs` from `1L` for classes that require it. (by @attipaci) - Various smaller fixes and improvements throughout, increased unit test coverage, and more comprehensive unit tests. (by @attipaci) - A lot of the Javadoc API documentation has been revised and improved. (by @attipaci) ### Added - [#177] Added support for preallocated blank header space, as per FITS 4.0 standard. via `Header.ensureCardSpace(int)` and `Header.getMinimumSize()`. Headers remain rewritable in-place as long as they don't exceed their original size in the file. (by @attipaci) - [#172] Added support for complex values in FITS headers, as specified by the FITS standard, via new `ComplexValue` class. (by @attipaci) - [#167] Added support for header integers in hexadecimal format, as specified by the FITS standard, e.g. via `addHexValue(...)` and `getHexValue(...)` methods in both `Header` and `HeaderCard` classes. (by @attipaci) - [#120] Added optional support for using `D` instead of `E` as the exponent in decimal representations (via `FitsFactory.setUseExponentD(boolean)` setting), as specified by the FITS standard. (by @wcleveland) - [#182] Replace fragmented `PrimitiveType...` hierarchy with a more aptly named one-stop `ElementType` class. The old hierarchy is also available, albeit in deprecated form. (by @attipaci) - [#191] Type safe `BITPIX` values via new `Bitpix` enum providing a restricted set. The unsafe `BITPIX` methods have been deprecated for removal in a future release. (by @attipaci) - [#175] Added new `Header.setParserWarningsEnabled(boolean)` option to log FITS standard violations when reading (3rd party) headers. (by @attipaci) - [#138] `FitsDate.equals()` / `hashCode()` / `compareTo()` implementations. (by @FinitePhaseSpace, thanks to @HabbitBaggins) ### Changed - [#197] This release contains numerous API changes and additions. While the source code is generally back-compatible with previous versions of this library for compiling, some method signatures have changed, and as a result the JAR should not be used as a drop-in replacement for applications that were compiled against earlier versions. To use version 1.16.0 of this library you should always compile your application against it. (by @attipaci) - [#161] Long strings enabled by default (FITS 4.0 standard). (by @attipaci) - [#195] Permissive default `FitsFactory` settings: error-free reading of some flawed 3rd party files by default (as long as they can be made sense of). However, issued encountered with 3rd party FITS files are logged so they can be inspected. Added new `FitsFactory.setDefaults()` method to restore default settings more easily. (by @attipaci) - [#125] Set `FitsFactory.useHierarch(true)` by default. `HIERARCH` style keys are written upper-case only by default, but case-sensitive support can also be enabled via a call to the `setCaseSensitive(boolean)` method of the `IHierarchKeyFormatter` instance used by `FitsFactory`. (by @olebole) - [#169] More predictable explicit precision control for header decimals. The same number of decimal places are shown after the leading figure regardless whether fixed-decimal or scientific (exponential) notation is used. (by @attipaci) - [#173] Fully preserve long comments for string values, including internal spaces in the comment, using the now standard long string convention. (by @attipaci) - [#170] Simpler, better methods for adding creating comment and history entries in headers, such as via `Header.insertComment(String)` or `.insertHistory(String)`, or via `HeaderCard.createCommentCard(String)` or `.createHistoryCard(String)`. (by @attipaci) - [#170] `Header.addValue(...)` and `Header.insert...(...)` methods now return the newly created `HeaderCard` objects for convenience. (by @attipaci) - [#121] More predictable header card ordering when editing headers, both directly or indirectly via an iterator. (by @attipaci) - [#188] `FitsHeap` access made a lot more efficient with true random access. (by @attipaci) - [#164] Source code updated for Java 8, with diamond operators and try-with-resources used throughout as appropriate. (by @attipaci) ### Deprecated - [#192] New FITS IO class hierarchies for better layering and separation of functionality. Standard IO functions (for reading, writing, positioning, and skipping) now conform to their canonical contracts in the core Java API. The messy old IO API is also supported, though deprecated, to provide back compatibility until the next major release. The new IO classes are also 2 to 3 times faster than before. (by @attipaci) - Deprecated classes and methods that (a) were exposed in the public API even though they should not have been, (b) had names that poorly reflected their function, (c) were poorly conceived/designed in the first place, and/or (d) were prone to misuse with unpredictable results. The deprecated API remains supported nonetheless, and slated for removal in the next major release (2.0) only. (by @attipaci) ## [1.15.2] - 2017-04-28 Maintenance release with bug fixes. ### Fixed - [#112] `ImageHDU` tiler corrupts values after 2GB worth of data bug fixed. (by @ritchieGitHub) - [#108] Non standard `BITPIX` allowed during de/compression. (by @ritchieGitHub) - [#107] Add tiler support for `ImageHDU` from uncompressing a `CompressedImageHDU`? (by @ritchieGitHub) - [#106] Remove redundant spaces in `HIERARCH` keys . (by @ritchieGitHub) - [#105] Fix integer overflow in case of negative values in combination with a defined blank value of `Integer.MIN_VANUE`. (by @ritchieGitHub) - [#104] make the worker threads deamons so they do not hold of a shutdown enhancement. (by @ritchieGitHub) - [#98] Update Outdated documentation in introduction enhancement, thanks to MaxNoe. (by @ritchieGitHub) - [#90] Fix Reading `boolean` arrays with `getColumn` bug. (by @ritchieGitHub) ### Added - [#113] `Header` can be controlled to specify the header card order. (by @ritchieGitHub) ### Changed - Maintenance release with bug fixes. ## [1.15.1] - 2016-08-19 Maintenance release with bug fixes. ### Fixed - [#102] Comment type header cards where not protected against to long comments, that can result in corrupted headers. (by @ritchieGitHub) - [#101] Introduction document verified and corrected kind thanks to Maximilian Nöthe. (by @MaxNoe) ### Changed - Maintenance release with bug fixes. ## [1.15.0] - 2016-08-07 Table compression activated. ### Added - [#61] Binary table compression now fully supported. (by @ritchieGitHub) - [#70] The dummy compression algorithm `NOCOMPRESS` is now supported. (by @ritchieGitHub) ### Changed - Binary table compression and tiling are now fully supported by nom-tam-fits. An API for easy handling of compressed tables is now provided. - [#96] Multiple code quality fixes, provided by various developers. ## [1.14.3] - 2016-06-05 Maintenance release with minor bug fixes. ### Changed - Maintenance release with bug fixes. - [#92] Removal of redundent attribute `rowSize`. Attention here the public api has a minor change, the `deleteColumns` in the `ColumnTable` does not return an `int` anymore. (by @ritchieGitHub) - [#91] Fix for a bug in encurling the multim arrays of the `BinaryTable` with variable length columns. (by @ritchieGitHub) ## [1.14.2] - 2016-03-11 Maintenance release with minor bug fixes and enhancements. ### Changed - Maintenance release with important bug fixes and the restoration of java 6 support. - [#84] `Fits` does not handle comments that start with 8 blank characters correctly when reading/writing/reading bug. (by @ritchieGitHub) - [#80] Restored Java 6 compatibility. (by @ritchieGitHub) ## [1.14.1] - 2016-02-24 Maintenance release with minor bug fixes and enhancements. ### Fixed - [#76] In case of long strings the difference between a null comment and an empty string was not detected correctly. (by @ritchieGitHub) ### Added - [#60] Image compression support for the null pixel mask, this allows correct `NaN` with the use of lossy compression's. (by @ritchieGitHub) ### Changed - Maintenance release with minor bug fixes and enhancements. - [#79] Important note for all users, since 1.13.0 a bug is fixed in the table behavior. This can cause problems for users expecting the "buggy" result. See the issue on github for more details. (by @ritchieGitHub) - [#77] Since a approximately 1.12.0 nom-tam-fits uses `java.util.logging` for all logs, the details what and where to log to can therefore be configured freely. (by @ritchieGitHub) ## [1.14.0] - 2016-01-10 full Image compression support ### Fixed - [#26] Wrong checksum calculation corrected. (by @ritchieGitHub) - [#54] Some problems with data segments that are bigger than 2GB corrected. (by @ritchieGitHub) - [#62] Header parsing performance optimization. (by @ritchieGitHub) - [#68] Comment style cards with a empty key value can now be used multiple times. (by @ritchieGitHub) ### Added - [#48] Added a [de]compression API supporting all compression methods in the proposed updates to the FITS standard. (by @ritchieGitHub) - [#72] The formatting of hierarch card keys can mow be be controlled. Two formats are provided. (by @ritchieGitHub) ### Changed - Image compression and tiling are now fully supported by nom-tam-fits. A 100% Java implementation of the compression libraries available in cfitsio was implemented. An API for easy handling of compressed images is now provided. Support for binary table compression and the `NULL_PIXEL_MASK` features is anticipated in the next release. - When [de]compressing all available CPU's are automatically utilized. - Internal compression allows FITS files to be created where the data are efficiently stored, but the metadata is still easily accessible. The tiling of images is particularly critical for supporting efficient access to subsets of very large images. A user can easily access only the tiles that overlap the region of interest and can skip data not of interest. While some skipping might be possible with uncompressed FITS files (i.e., read only the rows overlapping the desired subset), internal tiles can be much more efficient when the image is substantially larger than the subset. Most compression algorithms interfere with the ability to skip uninteresting data, but tiles are compressed independently, so users can benefit both from the compression and the selection of only a subset of the image. - [#68] Alignment of hierarch headercard values deactivated. (by @ritchieGitHub) ## [1.13.1] - 2015-08-21 Maintenance release with fixes for hierarch/longstring and rewrite bugs ### Fixed - [#46] After the correction of #44 the padding calculation of the fits header was wrong, now the calculation is consistent. (by @ritchieGitHub) - [#44] Improved the calculation of the number of cards to be used for a longstring in case of a hierarch cards because it was wrong when some special string lengths where used. Now rewriting is useing the new calculation to see if the header fits in place. (by @ritchieGitHub) - [#43] More variants of the hierarch keywords as in #16. (by @ritchieGitHub) - [#16] More variants of the hierarch keywords allowed (lowercase and dot), but writing will convert the keywords back to the standard. (by @ritchieGitHub) ### Changed - Maintenance release with fixes for hierarch/longstring and rewrite bugs. ## [1.13.0] - 2015-07-17 This is a stability release, before the new fits standard will be implemented in version 2.0 ### Fixed - [#24] Fixed handling of binary tables built from an empty state row by row. Fixed coupling of binary tables and the FITS heap to allow copying of binary tables using the internal ColumnTable representation. - [#20] Compression dependecy to apache compression is now optional again. (by @ritchieGitHub) - [#12] When reading/writing the same card the comment moved one blank to the right. (by @ritchieGitHub) - [#29] All javadoc's are now java-8 compatible and produce no warnings. (by @ritchieGitHub) ### Removed - [#23] Tile compression, will be implemented from scratch in 2.0 and is not yet available in 1.13.0 (by @ritchieGitHub) ### Changed - Major updates to the util package including a set of routines for efficient copying of arrays of all types. Some of the special FITS values are collected in the FitsIO class. New buffering utility classes have also been created. Some methods that were public in the util package but which were not used in the FITS library have been removed. The logging is now using `java.util.logging`, no standard out or stanard error is used anymore. - Added utilty class for updating checksums - Added examples in utilities package for how to use new Header enumerations. - [#35] Builder pattern for the creation of cards introduced. (by @ritchieGitHub) - [#23] Reorganized compression and added internal compression package. (by @ritchieGitHub) - [#29] Unit tests extended to cover 92% of the library . (by @ritchieGitHub) - [#17] Longstring support was improved and longer comments are now supported. (by @ritchieGitHub) - [#15] Support for biginteger and bigdecimal. (by @ritchieGitHub) - [#14] Generic detection of the value type of a card. (by @ritchieGitHub) - [#7] Insert a header card at a specific position. (by @ritchieGitHub) - [#36] All internally used keyword references are now enumerations. (by @ritchieGitHub) - [#37] Comment mapping is now moved to the standard keyword enumeration. (by @ritchieGitHub) - [#21] The settings of `FitsFactory` are now changeable to thread local specific settings. (by @ritchieGitHub) ## [1.12.0] - 2015-02-20 ### Added - Enumerations where added for ~1000 more or less Standard fits headers are now included (making compile references to headers possible). (by @ritchieGitHub) - Moved the sources to github and the artifacts to the central repo. (by @ritchieGitHub) - Moved the license to maven as build system, incl reorganisation of the project. tests are no included in the published jars any more. (by @ritchieGitHub) - Creation of a project site on github with issue management. (by @ritchieGitHub) - Richard van Nieuwenhoven joined the team. (by @tmcglynn) ### Changed - Moved the license to the official unlicensed. (by @ritchieGitHub) - Java formatting/licence updating is now done by the maven plug ins. (by @ritchieGitHub) - Moved the code to Java 1.6 and @Override is now consistent. (by @ritchieGitHub) ## [1.11.1] - 2014-07-07 ### Changed - Debuggging statements inadverently left in V111.0 were removed. (by @tmcglynn) ## [1.11.0] - 2013-06-07 ### Fixed - Fixed error in handling of strings with non-terminated quotes so that these don't crash program (thanks to Kevin McAbee) (by @Kevin McAbee) ### Added - The source code JAR (`fits_src.jar`) includes a number of new classes for which the corresponding class files are not included in `fits.jar`. These classes are pre-alpha versions of support for tile compressed data that is being developed. Interested Users may take a look at these, but they definitely are not expected to work today. Support for Rice, Gzip and HCompress compression is expected. (by @tmcglynn) ### Changed - Deferred allocation of memory when creating `FitsHeap`s (thanks to Vincenzo Forchi, ESO). (by @Vincenzo Forchi) - Fixed error in getting size of associated data for dummy headers in `Header.getDataSize()`. This could return 1 rather than 0 if `NAXIS=0` was used to signal a dummy. This doesn't affect data when read normally (as full HDUs) since the `ImageData` class does the computation correctly. (Thanks to Pat Dowler, CADC) (by @Pat Dowler) ## [1.10.0] - 2012-10-25 ### Changed - No functional changes to the FITS code are included in this release. All internal documentation has been updated to reflect that this library is now available in the public domain. (by @tmcglynn) ## [1.08.1] - 2012-02-01 ### Fixed - Fixed error in the writing of ASCII table columns where all of the elements of the table were null or 0 length strings. Previously we would write a column with `TFORMn = 'A0'`. This is not supported by CFITSIO and since it is not valid Fortran is of dubious legality for FITS. Such columns are now written with `A1` (issue noted by Jason Weiss, UCLA). (by @Jason Weiss) - `AsciiTable` did not check if columns were of a valid type (if the FitsFactory methods were used, then a `BinaryTable` would be written, but a user can explicitly instantiate an `AsciiTable`). A `FitsException` is now returned if a column other than a `String`, `double`, `int` or `long` array is used (by @tmcglynn) ## [1.07.0] - 2012-01-20 ### Added - Added `boolean hadDuplicates()` and `List getDuplicates()` methods to Header to allow users to track if there were duplicate keywords in a header. (by @tmcglynn) ## [1.07.0] - 2011-11-11 ### Fixed - Fixed the order of the `EXTEND` keyword to follow the `NAXISn` keywords when it is specified. This constraint on the `EXTEND` keyword is no longer required in the latest version of the standard, but it doesn't hurt anything and may make the file more acceptable to some readers. (by @tmcglynn) - Fixed JavaDoc errors for a number of files. (by @tmcglynn) - Fixed a bug in `Header.rewriteable()` related to the same issue as the original size. (by @tmcglynn) ### Changed - This release contains some new features suggested by Booth Hartley (IPAC) who supplied modified code including: (by @Booth Hartley) - Allow a FITS file to have invalid data after a valid FITS HDU. User can call `FitsFactory.setAllowTerminalJunk(true)` to enable this. The `Fits` object will return all valid HDUs. Note that whatever follows the valid FITS data must start with something that is clearly not FITs. This includes modifications to `FitsFactory` and `Header`. (by @tmcglynn) - Allow users to find original size of headers as they were read from some source file. The library throws away duplicate key values, so that the number of header cards in the header as read may be smaller than the original data. The `getOriginalSize()` gets the original size of the header in bytes. A `resetOriginalSize()` allows the user to tell the library that this header has been updated on disk and now has the same number of records as internally. (by @tmcglynn) - Updated `Fits.setChecksum` so that it will now set both the `CHECKSUM` and `DATASUM` keywords. (by @tmcglynn) - Added tests for the new capabilities above and updated the checksum test. (by @tmcglynn) ## [1.06.0] - 2011-05-23 ### Fixed - A bug in the `UndefinedData` class was detected Vincenzo Forchi and has been corrected. (by @V. Forchi) ### Changed - Substantial reworking of compression to accommodate BZIP2 compression. The Apache Bzip library is used or since this is very slow, the user can specify a local command to do the decompression using the `BZIP_DECOMPRESSOR` environment variable. This is assumed to require a '-' argument which is added if not supplied by the user. The decompressor should act as a filter between standard input and output. (by @tmcglynn) - User compression flags are now completely ignored and the compression and the compression is determined entirely by the content of the stream. The Apache library will be needed in the classpath to accommodate BZIP2 inputs if the user does not supply the `BZIP_DECOMPRESSOR`. (by @tmcglynn) - Adding additional compression methods should be much easier and may only involve adding a couple of lines in the `FitsUtil.decompress` function if a decompressor class is available. (by @tmcglynn) - One subtle consequence of how compression is now handled is that there is no advantage for users to create their own `BufferedDataInputStream`s. Users should just provide a standard input stream and allow the FITS library to wrap it in a `BufferedDataInputStream`. (by @tmcglynn) - The `nom.tam.util.AsciiFuncs` class now handles ASCII encoding to more cleanly separate this functionality from the FITS library and to enable Java 1.5 compatibitity. (Suggested by changes of L.Bourges) Other V1.5 incompatiblities removed. (by @tmcglynn) - The `HeaderCommentsMap` class is now provided to enable users to control the comments that are generated in system generated header cards. The map is initialized to values that should be the same as the current defaults. This should allow users to emulate the comments of other packages. (by @tmcglynn) - All Java code has been passed through NetBeans formatter so that it should have a more uniform appearance. (by @tmcglynn) ## [1.05.1] - 2011-02-16 ### Fixed - The implementation of long string values was incorrect, using `COMMENT` rather than `CONTINUE` cards. Noted originally by V. Forchi. (by @V. Forchi) - The placement of the header cursor after opening a primary array was such that unless the user took explicit action to move the cursor, new header records would be written before the `EXTEND` keyword which is a violation of the FITS standard (although it would not affect the operations of this library). The library now leaves the cursor just after the `EXTEND` keyword where new keywords are legal. It's still possible for users to write an illegal header but now it requires at least a little effort on their part. Noted originally by V. Forchi. (by @V. Forchi) ### Changed - This build procedure for FITS library has been changed. The library is now stored as a NetBeans project and the standard NetBeans build script has been modified to generate the `fits.jar` and `fits_src.jar`. The names of a number of the test procedures have been slightly modified (`XXXTester` -> `XXXTest`) and test data are included in the class jar file. (by @tmcglynn) ## [1.05.0] - 2010-12-12 ### Changed - Adding methods to allow finer control of the placement of metadata records for columns of FITS tables. This could previously be done using `Cursor`s, but the `TableHDU.setTableMeta()` methods now allow these to be specified more directly. This involves changes only to `TableHDU`. Usage is illustrated in the test method `BinaryTableTest.columnMetaTest`. (by @Laurent Bourges) - Adding more rigor to the transformation between bytes and strings and fixing a bug in the handling of strings with embedded nuls. According to the standard an embedded null should terminate an string in a binary table.by Laurent Bourges The standard also precludes other non-printing characters from strings. This has been ignored previously, but there is now a method `FitsFactory.setCheckAsciiString(boolean flag)` which can be called to turn on checking. A warning will be issued and non-printing characters will be converted to spaces if this flag is set. Only a single warning will be issued regardless of the number of invalid characters are seen. There are changes in a number of classes where the conversions occur to ensure that the ASCII charset is used. How these changes work is illustrated in `BinaryTableTest.specialStringsTest`. (by @Laurent Bourges) - Handling fixed and variable length, single and double precision complex data. The library uses a `float[2]` or `double[2]` for a complex scalar. This is mostly bug fixes to existing code and changes are only in `BinaryTable` and `BinaryTableHDU`. The method `BinaryTableHDU.setComplexColumn()` allows the user to tell the FITS writer that a field which otherwise would be treated as a float or double array (with most rapidly varying dimensionality of 2) should be treated as complex. The internal data representations are identical. Variable length complex data will be found automatically if number of elements actually varies. A variable length complex column is a 3-D float or double array where the last dimension (for Java) is always 2, the first dimension is the number of rows in the table and the middle dimension is the number of complex numbers in the row and may vary from row to row. Other variable length columns are represented as 2-D arrays, where the first index points to the row, and the second index enumerates the elements in the row. Use of complex columns is illustrated in `BinaryTableTest` in the routines `testSimpleComplex` (fixed columns), `testVar` (variable length columns), and `buildByColumn` and `buildByRow` where columns and rows containing complex numbers are added to existing tables. (by @Laurent Bourges) - Changing the null HDU created when a table is to be written without a prior image to use a vector with dimensionality 0 rather than a one-dimensional vector with a dimension of 0. I.e, use `NAXIS=0` rather than `NAXIS=1`, `NAXIS1=0`. (by @Laurent Bourges) - Consolidating the writing of padding at the end of FITS elements into the `FitsUtil.pad` methods. (by @tmcglynn) - Adding the `reset()` method to the `FitsElement` interface. This attempts to reset the Fits input stream pointer to the beginning of the element. It does not throw exceptions but will return `false` if not successful. This is intended to make it easier for user who wish to use low-level I/O to read FITS data, by allowing them to position the stream to the beginning of the data they are interested in. (by @tmcglynn) - Changed `FitsUtil.HDUFactory(Object x)` to accept a Header object as well as the various kinds of data inputs. (by @tmcglynn) - Provided a method in `BinaryTabl`e to get back the ModelRow array. This makes is easier for users to do low level I/O in binary tables. An `ArrayDataInput` object will read a row of the table, given the result of `getModelRow()`. (by @tmcglynn) - Added a `getColumns()` method to `TableHDU`. This returns an `Object[]` array where each entry is the result of `getColumn(n)` (by @tmcglynn) ## [1.04.0] - 2009-12-24 ### Fixed - A bug in the processing of keyword values with embedded apostrophes was fixed. Apostrophe's were properly doubled in encoding but the doubling was left when the values were read. (by @tmcglynn) - A potential bug in the processing of headers discovered by Mark Taylor was fixed. (by @Mark Taylor) ### Changed - Support for the HEASARC long strings convention has been added. This affects only the `Header` class. Two new public static methods have been added. `setLongStringsEnabled(boolean)` allows the use to enable/disable the handling of long strings. `getLongStringsEnabled()` returns the current setting. By default long strings are disabled. The convention is enabled automatically whenever a header is read which has the `LONGSTRN` keyword is read. It is not disabled if subsequent headers are read which do not have this keyword. The `addValue(String,String,String)`, `getStringValue(String)` and `removeCard(String)` methods are affected, allowing the user to set, read and delete long string values. The library does NOT ensure that users do not interpolate new keywords or comments inside the card sequence that is used to store the long string value. (by @tmcglynn) ## [1.03.1] - 2009-07-27 ### Added - The implementation of the `FitsUtil.byteArrayToStrings` method was changed so that trimmed space from strings can be cleaned up more efficiently. Change suggested by J.C. Segovia (ESA). There should be no effect -- other than memory usage -- on external programs. (by @J.C. Segovia) ### Changed - Users might want to note that when reading string values in binary tables, both leading and trailing spaces are trimmed from the string values. (by @tmcglynn) ## [1.03.0] - 2009-07-22 ### Fixed - A bug in the new `PaddingException` was fixed which allows use of Tilers with truncated Image HDUs. (by @tmcglynn) ### Added - This release adds further support for large datasets where the size of an HDU may exceed 2GB. In `ArrayDataInput` (and the `BufferedFile` and `BufferedDataInputStream` that implement it) int `skipBytes(int)` method of `java.io.DataInput` is now overloaded with `long skipBytes(long)`. In `ArrayFuncs` `int computeSize(Object)` method is augmented with `long computeLSize(Object)`. It was not possible to use the same name here since the argument type is the same. Similarly int `nElements(Object)` is now matched with long `nLElements(Object)`. These changes should not affect current usage of the existing methods. References to `skipBytes` and `computeSize` in the FITS classes now take advantage of these new methods. While these changes increase the support of the library for large datasets, there are still a number of restictions that arise from Java's limit that array indices must be `int`s. E.g., no single dimension can exceed 2 GB, and the total size of the heap for a given binary table HDU cannot exceed 2 GB. ASCII tables may also be limited to 2 GB in some circumstances. Files which exceed these limits may be readable using line by line approaches, but users will need to use the library at a much lower level. (by @tmcglynn) - The `Header.read()` method may now throw an `IOException` in circumstances where it would previously throw an `Error`. It probably should throw a `FitsException`, but that was not in the signature and might have broken existing programs. (by @tmcglynn) ### Changed - Some obsolete comments indicating that `BITPIX=64` was an extension of FITS were deleted. FITS has officially supported longs for a fair number of years now. (by @tmcglynn) - The regression tests have been augmented to test the new features, but users should note that the new BigFileTester test takes a very long time to run. E.g., on the primary development machine this takes 240 seconds while all of the other tests finish in just a few seconds. The time is simply the time it takes to write a file of known content that is more than 2 GB in size. (by @tmcglynn) ## [1.02.0] - 2009-07-08 ### Changed - ASCII tables with data fields that were blank filled were not being handled properly. According to the FITS standards, numeric fields where the FITS table has blanks are to be treated as containing 0. A parsing error was being returned. The `getInt`, `getLong`, and `getDouble` methods in `ByteParser` were changed to acoommodate this case (`getFloat` simply calls `getDouble`). (by @L. Michel) - A new exception, `PaddingException`, which inherits from `FitsException` has been added. This exception is thrown when an otherwise valid HDU is not properly padded to the next 2880 byte boundary. The exception class has a `getTruncatedHD`U method which allows the user to get the information in the truncated HDU. In addition to the new class changes were made in `BinaryTable`, `AsciiTable`, `UndefinedData`, `ImageData` and `RandomGroupsData` to throw the exception at the appropriate time. The main Fits method was also updated so that when its `readHDU()` method is being used, the notional header that is given to the truncated HDU in the `Data` classes is replaced by the actual header. If a user wishes to ignore padding exceptions, then a FITS file may be read using the following idiom in the new `nom.tam.fits.test.PaddingTester` to see a complete example of this idiom. (by @tmcglynn) ## [1.01.0] - 2009-06-24 ### Fixed - A bug noted by Thomas Granzer in the handling of `HIERARCH` keyword values was also corrected. (by @tmcglynn) ### Changed - A number of changes were implemented to handle large FITS files more gracefully and to correct bugs associated with large files. This includes a change to the method `Data.getTrueSize()`. This method was public only for the `BinaryTable` data type and previously returned an `int`. It now returns a `long`. User programs which called this method will need to be recompiled. Specific bugs were noted by Javier Diaz and Juan Carlos Segovia. Note that the program may still fail on very large files but it should give more informative error messages when it does so (by @tmcglynn) ## [1.00.2] - 2009-03-09 ### Fixed - Fixed bug where reading a table by rows caused reading a subsequent HDU to fail. Added tests to `BinaryTableTester` and `HeaderCardTester`. (by @tmcglynn) ## [1.00.1] - 2009-02-19 ### Fixed - Fixed bug where exponential notation in FITS header keywords used `e` rather than `E` (noted by Javier Diaz) (by @Javier Diaz) ## [1.00.0.1] - 2008-07-11 ### Added - The major chage to this release is support for `.Z` compressed images. A problem reading past the end of files in normal FITS processing was included in the 1.0 release and was fixed (7/11/08). The earlier jars were overwritten. The problem shows up in the regression test suite. (by @tmcglynn) ## [1.00.0] - 2008-06-10 ### Fixed - Bug fix to `BinaryTable` by A. Kovacs (by @A. Kovacs) ### Added - The major chage to this release is support for `.Z` compressed images. This is implemented by using the uncompress command which must be in the user's execution path. (by @tmcglynn) ### Changed - There is much more dynamic checking of the magic number of inputs to determine whether the input to the FITS constructor is compressed or not, and if compressed what the compression type is. This can still be confused but in many cases it will get the compression right regardless of what the user specifies. Future versions may completely ignore the user specified compression flag. (by @tmcglynn) ## [0.99.3] - 2007-12-04 ### Fixed - Binary table handling of 1 character strings. (by @tmcglynn) ## [0.99.5] - 2007-12-04 ### Fixed - Additional fixes for zero length and `null` strings (by @tmcglynn) - Fix to handling of Strings in Binary tables (A. Kovacs) (by @A. Kovacs) ### Changed - Added Support HTTPS, FTP and FILE URLs (by @tmcglynn) - Added `Fits(String,compressed)` constructor (by @tmcglynn) - Made some of the methods in `FitsFactory` public. (by @tmcglynn) - Added `getRawElement` method to `BinaryTable`. (by @tmcglynn) - Changed handling of `double` values in header so that they all fit into the fixed format. In rare circumstances this may result in a loss of precision. (by @tmcglynn) ## [0.99.3] - 2006-12-21 ### Changed - Additional changes to handle `null` and zero length strings. (by @tmcglynn) ## [0.99.2] - 2006-12-15 ### Fixed - `AsciiTable`: Setting a row, column or element de-nulls any elements that were set to `null`. Fixed offsets in columns after column was deleted. (by @tmcglynn) - `FitsUtil`: Fixed bug in `maxLength` which looked for nulls in the array pointer rather than the individual strings. Added check for nulls in `stringsToByteArray` (by @tmcglynn) - `HeaderCard`: Truncated String in one argument constructor to a maximum of 80 characters. (by @tmcglynn) - `BinaryTable`: Fixed handling of columns with 0 width (e.g., 0 length strings, or arrays of 0 length. (by @tmcglynn) - `ColumnTable`: Fixed handling of columns with 0 width. (by @tmcglynn) ### Added - `ArrayFuncs`: Added `arrayEquals()` methods which allow comparison of arrays of arbitrary dimensionality. Used extensively in the updated test classes. (by @tmcglynn) ### Changed - Moved code to use subversion repository and Ant compile scripts. (by @tmcglynn) - Major transformations of all test code to use Junit and automated checking rather than comparing print outs. (by @tmcglynn) - `nom.tam.fits.utilities` package created and `FitsCopy` and `FitsReader` classes were moved there. (by @tmcglynn) - A few test classes, e.g., `BigImage` and `RMFUpdTest` were deleted and their functions subsumed into the other tests. (by @tmcglynn) - Test routines now considered standard part of library. There are not separate JARs for the test routines. (by @tmcglynn) - Note that the test routines use Annotations and may not compile with versions of Java prior to 1.5. (by @tmcglynn) ## [0.99.1] - 2006-07-29 ### Fixed - A number of errors in the handling of variable length arrays were fixed. These were pointed out by Guillame Belanger. This included changes to `util.ColumnTable` but mostly `BinaryTable` and `FitsHeap`. (by @tmcglynn) ### Changed - Added new methods to delete rows and columns from both binary and ASCII tables. There are changes to many of the table classes including `util/ColumnTable`. These changes were suggested by row deletion code written by R. Mathar, but the actual implementation is entirely independent and errors are handled somewhat differently than in his code. There are `deleteColumns` and `deleteRows` methods in `TableHDU` that delete either a specified range or all tables or columns after (and including) the one specified. (by @tmcglynn) - The `util.HashedList` implementation has been completely revised. It now uses a `HashedMap` for keyed access and an `ArrayList` for sequential access. It no longer implements a simple but custom list structure. The public interface was not significantly changed. (by @tmcglynn) - `Header` now sorts keywords before a header is written to ensure that required keywords come where they need to be. Previously users needed to work to make sure that they wrote required keywords in the right location in the header. A new class, `HeaderOrder`, is used. (by @tmcglynn) - A number of changes mostly to `BinaryTable` or documentation in other routines suggested by R. Mathar. (by @R.J. Mathar) ## [0.99.0] - 2006-06-23 ### Fixed - Corrected bug in writing a binary table when the read of that table had been deferred. (by @tmcglynn) ### Added - Added support for Checksums. Use the `setChecksum` methods in the FITS class to add checksums to FITS HDUs. The static method `setChecksum(HDU)` adds a checksum to a given HDU. The instance method `setChecksum()` adds checksums to all HDUs in the file. Note that setting the checksum should be the last step before writing the file since any manipulation of the file is likely to invalidate the checksum. (This code was contributed by R.J. Mathar, Leiden University). (by @R.J. Mathar) - Changed handling of 1-d arrays with a single element so that they can be distinguished from scalar values. No `TDIMn` will be be created for scalar columns, and a `TDIMn = '(1)'` will give an array rather than a scalar value. (Suggested by Jorgo Bakker. ESA) For data written using the previous version of the FITS library, this may cause problems when the data is read with the new version, since the type of the returned column will be different. (by @tmcglynn) - When checking if a file is compressed, the actual content of the file will be used if possible rather than the name (Suggested by Laurent Michel, CDS) (by @Laurent Michel) - The `ArrayFuncs.newInstance` method now accepts an dimension array of length 0 and returns a 1-d array of length 1 to emulate a scalar. (by @tmcglynn) ### Changed - The three packages, `nom.tam.fits`, `nom.tam.util` and `nom.tam.image` have been combined into a single JAR file for the convenience of the user. (by @tmcglynn) - The code used to support `TFORMn = 'xNNN'` where the array dimension followed rather than preceded the format type. This has been deleted (Suggested by Laurent Michel, CDS) (by @Laurent Michel) - Zero-length string values should now be allowed as header keyword values (Bug noted by Fred Romelfanger, ST ScI and Jorgo Bakker, ESA). (by @Fred Romelfanger and Jorgo Bakker) - The addLine methods in `Header` are now `public` rather than `protected`. (by @tmcglynn) - If the `Fits.write()` method is called using a `BufferedFile`, then the size of the file is truncated at the end of the write. Otherwise if the FITS data was being written into a previously existing file of greater length, there would be extra bytes at the end of the file. This is still possible if the user uses the write methods for individual constituents of the FITS object. (by @tmcglynn) ## [0.97] - 2003-11-01 ### Fixed - Version 0.97 corrects several bugs in the handling header keywords and ASCII tables and Images. (by @tmcglynn) - The handling of the `EXTEND` keyword has been made consistent with FITS standards (by @tmcglynn) - ASCII tables are first read to an intermediate byte buffer and then parsed as needed. Bugs where this buffer was being deleted at inappropriate times, or left undeleted when it was invalid were fixed. This should fix errors when AsciiTables are read from non-seekable sources. This should slightly speed up most access to ASCII tables (by @tmcglynn) - In certain circumstances an Image would not be properly initialized before it was to be written (by @tmcglynn) - The routines `Header`, `HeaderCard`, `ImageData` and `AsciiTableData` where modified in this release (by @tmcglynn) ### Added - The `getChannel` method was added to `BufferedFile` (by @tmcglynn) ### Changed - The `HeaderCard` class now has constructor with the signature `(String,String,boolean)` which may be used to generate either a comment style card with the keyword and value given, or a card with a `null` value (by @tmcglynn) ## [0.96] - 2003-03-20 ### Fixed - A bug in the creation of ASCII Table Headers was fixed. Some of the header cards in the header were being inserted as if they were comments, allowing multiple copies to be generated. This was also possible when a Header was created from an array of strings. (by @tmcglynn) ### Changed - The handling of `PCOUNT`, `GCOUNT` and `EXTEND` keywords was changed in images so that the first two are only generated for extensions and the first only for primary HDU's. (by @tmcglynn) ## [0.93] - 2001-01-01 ### Fixed - Several bugs relating to null images were corrected. (Thanks to Jens Knudstrup) (ImageData) (by @Jens Knudstrup) - The handling of EOF conditions in array reads in the `BufferedFile` and `BufferedDataInputStream` classes was made consistent with the behavior of `java.io` classes reading byte arrays (by @tmcglynn) - Several bug fixes implemented by Alan Brighton (and already fixed in the Jsky distribution) were incorporated (by @tmcglynn) - All references to the `java.lang.reflect.Array.newInstance()` methods were modified to use new methods with the same signature in `ArrayFuncs`. These new methods throw an `OutOfMemory` exception when an array cannot be created. The JVM methods seem -- in contradiction to the documentation -- to simply return `null`. Previously the program could mysteriously crash when used to read large files, when the `null` in a dynamic allocation was eventually dereferenced (by @tmcglynn) ### Added - The `HeaderCard` class has been modified to handle The `HIERARCH` keyword convention. The `FitsFactory` now has methods `set`/`getUseHierarch` to enable/disable this processing (by @tmcglynn) - A new interface `FitsElement` has been added which is implemented by the `BasicHDU`, `Header`, `Data` and `FitsHeap` classes. It enables users to more easily deal with FITS data at the byte level. There is also a public method `getDataSize` in `Header` to get the size in bytes of the associated data element including padding. The `FitsHeap` class has been made public (by @tmcglynn) ## [0.92] - 2000-10-12 ### Changed - `BinaryTable` Fixed bug initializing `BinaryTable`'s read from streams (rather than files) (by @tmcglynn) ## [0.91] - 1996-01-02 ### Changed - `FitsDate`: added `getFitsDateString` (by @tmcglynn) - `Header`: `FitsDate`: made several methods public (by @tmcglynn) - added checking for initial keywords before write (by @tmcglynn) - `BinaryTable`: removed `TDIMn` keywords for variable length columns (by @tmcglynn) - `BinaryTable`: fixed bug that made `BinaryTable(Object[][])` constructor unusable (by @tmcglynn) - `BinaryTableHDU`: fixed usage of `THEAP` keyword (by @tmcglynn) - `AsciiTable`: use blanks for data filler rather than `null`s (by @tmcglynn) - `BasicHDU` made `getDummyHDU` public (by @tmcglynn) - `HeaderCard` fixed padding of string values which sometimes had one too many spaces (by @tmcglynn) - `image.ImageTiler` allow requests for tiles that are not fully within the original image (by @tmcglynn) - `util.ByteFormatter`: changed formatter to use `E` (rather than `e`) for exponents since `e` not legal for FITS ASCII tables (by @tmcglynn) ## [0.90] - 1996-01-01 ### Changed - Support for ASCII tables (by @tmcglynn) - Deferred input for images and tables (data is read only when user actually requests it) (by @tmcglynn) - Image subsetting without reading the entire image (by @tmcglynn) - Reading individual rows and elements of tables without reading the entire table (by @tmcglynn) - Support for in-place rewriting of headers and data (by @tmcglynn) - Transparent support for Strings in ASCII and Binary tables (by @tmcglynn) - Transparent support for booleans in binary tables, including varying length columns (by @tmcglynn) - Efficient buffered random access methods (by @tmcglynn) - More flexible support for I/O of primitive arrays (by @tmcglynn) nom-tam-fits-1.21.0/CODE_OF_CONDUCT.md000066400000000000000000000121361476377620500167310ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at NASA HEASARC. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. nom-tam-fits-1.21.0/CONTRIBUTING.md000066400000000000000000000101651476377620500163630ustar00rootroot00000000000000The _nom-tam-fits_ library is a community-maintained project. We absolutely rely on developers like you to make it better and to keep it going. Whether there is a nagging issue you would like to fix, or a new feature you'd like to see, you can make a difference yourself. We welcome you as a contributor. More than that, we feel like you became part of our community the moment you landed on this page. We very much encourange you to make this project a little bit your own, by submitting pull requests with fixes and enhancement. When you are ready, here are the typical steps for contributing to the project: 1. Old or new __Issue__? Whether you just found a bug, or you are missing a much needed feature, start by checking open (and closed) [Issues](https://github.com/nom-tam-fits/nom-tam-fits/issues). If an existing issue seems like a good match to yours, feel free to raise your hand and comment on it, to make your voice heard, or to offer help in resolving it. If you find no issues that match, go ahead and create a new one. 2. __Fork__. Is it something you'd like to help resolve? Great! You should start by creating your own fork of the repository so you can work freely on your solution. We also recommend that you place your work on a branch of your fork, which is named either after the issue number, e.g. `issue-192`, or some other descriptive name, such as `implement-foreign-hdu`. 3. __Develop__. Feel free to experiment on your fork/branch. If you run into a dead-end, you can always abandon it (which is why branches are great) and start anew. You can run your own test builds locally using `mvn clean test` before committing your changes. If the tests pass, you should also try running `mvn clean package` and `mvn site stage` to ensure that the package and javadoc are also in order. Remember to synchronize your `master` branch by fetching changes from upstream every once in a while, and merging them into your development branch. Don't forget to: - Add __Javadoc__ your new code. You can keep it sweet and simple, but make sure it properly explains your methods, their arguments and return values, and why and what exceptions may be thrown. You should also cross-reference other methods that are similar, related, or relevant to what you just added. - Add __Unit Tests__. Make sure your new code has as close to full unit test coverage as possible. You should aim for 100% diff coverage. When pushing changes to your fork, you can get a coverage report by checking the Github Actions result of your commit (click the Codecov link), and you can analyze what line(s) of code need to have tests added. Try to create tests that are simple but meaningful (i.e. check for valid results, rather than just confirm existing behavior), and try to cover as many realistic scenarios as appropriate. Write lots of tests if you need to. It's OK to write 100 lines of test code for 5 lines of change. Go for it! And, you will get extra kudos for filling unit testing holes outside of your area of development! 4. __Pull Request__. Once you feel your work can be integrated, create a pull request from your fork/branch. You can do that easily from the github page of your fork/branch directly. In the pull request, provide a concise description of what you added or changed. Your pull request will be reviwed. You may get some feedback at this point, and maybe there will be discussions about possible improvements or regressions etc. It's a good thing too, and your changes will likely end up with added polish as a result. You can be all the more proud of it in the end! 5. If all goes well, your pull-request will get merged, and will be included in the upcoming release of _nom-tam-fits_. Congratulations for your excellent work, and many thanks for dedicating some of your time for making this library a little bit better. There will be many who will appreciate it. :-) If at any point you have questions, or need feedback, don't be afraid to ask. You can put your questions into the issue you found or created, or your pull-request, or as a Q&A in [Discussions](https://github.com/nom-tam-fits/nom-tam-fits/discussions). nom-tam-fits-1.21.0/LICENSE000066400000000000000000000022731476377620500151400ustar00rootroot00000000000000This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to nom-tam-fits-1.21.0/README.md000066400000000000000000003016171476377620500154160ustar00rootroot00000000000000 ![Build](https://github.com/nom-tam-fits/nom-tam-fits/actions/workflows/build.yml/badge.svg) ![Testing](https://github.com/nom-tam-fits/nom-tam-fits/actions/workflows/test.yml/badge.svg) [![Package](https://github.com/nom-tam-fits/nom-tam-fits/actions/workflows/nexus.yml/badge.svg)](https://oss.sonatype.org/#nexus-search;quick~nom-tam-fits) [![Project Site](https://github.com/nom-tam-fits/nom-tam-fits/actions/workflows/site.yml/badge.svg)](https://github.com/nom-tam-fits/nom-tam-fits/actions/workflows/site.yml) [![Codecov](https://codecov.io/gh/nom-tam-fits/nom-tam-fits/branch/master/graph/badge.svg?token=8rFyA5YzE5)](https://codecov.io/gh/nom-tam-fits/nom-tam-fits) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/gov.nasa.gsfc.heasarc/nom-tam-fits/badge.svg)](https://maven-badges.herokuapp.com/maven-central/gov.nasa.gsfc.heasarc/nom-tam-fits) # User's guide __nom.tam.fits__ is a full-featured, fast, 100% pure Java 8+ library for reading, writing, and modifying [FITS files](https://fits.gsfc.nasa.gov/fits_standard.html). The library owes its origins to Tom A. McGlynn (hence the _nom.tam_ prefix) at NASA Goddard Space Flight Center. Currently it is maintained by [Attila Kovacs](https://github.com/attipaci) at the Center for Astrophysics | Harvard & Smithsonian. This document has been updated for 1.20 and/or later 1.x releases. ## Table of Contents - [Related links](#related-links) - [Introduction](#introduction) - [Compatibility with prior releases](#deprecated-methods) - [Reading FITS files](#reading-fits-files) - [Writing FITS data](#writing-data) - [Modifying existing FITS files](#modifying-existing-files) - [FITS headers](#fits-headers) - [Creating tables](#building-tables-from-data) - [Compression support](#compression-support) - [Release schedule](#release-schedule) - [How to contribute](#contribute) ----------------------------------------------------------------------------- ## Related links You may find the following links useful: - [API documentation](https://nom-tam-fits.github.io/nom-tam-fits/apidocs/index.html) - [FITS Standard](https://fits.gsfc.nasa.gov/fits_standard.html) - [History of changes](https://nom-tam-fits.github.io/nom-tam-fits/CHANGELOG.html) - [Project site](https://nom-tam-fits.github.io/nom-tam-fits/index.html) on github.io - [Maven Central repository](https://mvnrepository.com/artifact/gov.nasa.gsfc.heasarc/nom-tam-fits) ----------------------------------------------------------------------------- ## Introduction FITS (Flexible Image Transport System) is a binary format of many astronomical datasets and images. The library requires a level of familiarity with FITS and its common standards and conventions for effective use. For example, while the library will automatically interpret and populate the mandatory minimum data description in FITS headers, it will not automatically process many optional standard or conventional header entries. It is mostly up to the users to extract or complete the description of data to its full extent, for example to include [FITS world coordinate systems (WCS)](https://fits.gsfc.nasa.gov/fits_wcs.html), physical units, etc. Users are encouraged to familiarize themselves with the [FITS standard](https://fits.gsfc.nasa.gov/fits_standard.html) and conventions described therein to be effective users of this library. This is an open-source, community maintained, project hosted on GitHub as [nom-tam-fits](https://github.com/nom-tam-fits/nom-tam-fits). Further information and documentation, including API docs, can be found on the [project site](https://nom-tam-fits.github.io/nom-tam-fits/index.html). ### FITS data (HDU) types A FITS file is composed of one or more *Header-Data Units* (HDUs). Each HDU consists of a *header*, which describes the data and possibly contain extra metadata (as key-value pairs) or comments, and a *data* section. The current FITS standard (4.0) recognizes the following principal HDU / data types: 1. **Image** can store a regular array of 1-999 dimensions with a type corresponding to Java numerical primitives, such as a one-dimensional time series of samples (e.g. `int[]`), or a three-dimensional cube of voxels (e.g. `float[][][]`). (Note, that Java supports images up to 255 dimensions only but it's unlikely you'll find that limiting for your application.) 2. **Binary Table** can store rows and columns of assorted of elements. Each column entry may be either a single value, or a fixed-sized (multidimensional) array, or else a variable-length 1D arrays of a given type. All Java primitive numerical types are supported, but also `String`, `Boolean` (logical), `boolean` (bits), and `ComplexValue` types. 3. **ASCII Table** (_discouraged_) is a simpler, less capable table format with support for storing singular primitive numerical types, and strings only -- in human-readable format. You should probably use the more flexible (and more compact) binary tables instead for your application, and reserve use of ASCII tables for reading data that may still contain these. 4. **Random Groups** (_discouraged_) can contain a set of images of the same type and dimensions along with a set of parameters of the same type (for example an `int[][]` image, along with a set of `int` parameters). They were never widely used and the FITS 4.0 standard discourages them going forward, given that binary tables provide far superior capabilities for storing the same type of data. Support for these type of HDUs is basic, and aimed mainly at providing a way to access data that was already written in this format. 5. **Foreign File** can encapsulate various other files within the FITS. Foreign file HDUs are a recognized convention, but not (yet) officially part of the FITS standard. We do not explicitly support foreign file encapsulation yet, but it is something that we are considering for a future release. In addition to the basic HDU types, there are extension of table HDUs that serve specific purposes, such as: - **Compressed Images / Tables** are an extension of the binary table HDUs for storing an image or a binary table in a compressed format, with tiling support to make parts easily accessible from the whole. We provide full support for compressing and decompressing images and tables, and for accessing specific regions of compressed data stored in this format. - The **Hierarchical Grouping** convention is an extension of table HDUs (ASCII or binary) for storing information on the hierarchical relation of HDUs contained within (or external to) the FITS. The hierarchical grouping is a recognized convention, but not (yet) officially part of the FITS standard. We do not explicitly support this convention yet, but it is something that we are considering for a future release. ----------------------------------------------------------------------------- ## Compatibility with prior releases The current version of the __nom.tam.fits__ library requires Java 8 (or later). We strive to maintain API compatibility with earlier releases of this library, and to an overwhelming extent we continue to deliver on that. However, in a few corner cases we had no choice but to change the API and/or behavior slightly to fix bugs, nagging inconsistencies, or non-compliance to the FITS standard. Such changes are generally rare, and typically affect some of the more obscure features of the library -- often classes and methods that probably should never have been exposed to users in the first place. Most typical users (and use cases) of this library will never see a difference, but some of the more advanced users may find changes that would require some small modifications to their application in how they use __nom-tam-fits__ with recent releases. If you find yourself to be one of the ones affected, please know that the decision to 'break' previously existing functionality was not taken lightly, and was done only because it was unavoidable in order to make the library function better overall. Note, that as of __1.16__ we offer only API compatibility to earlier releases, but not binary compatibility. In practical terms it means that you cannot simply drop-in replace you JAR file, from say version __1.15.2__ to __1.19.0__. Instead, you are expected to (re)compile your application with the JAR version of this library that you intend to use. This is because some method signatures have changed to use an encompassing argument type, such as `Number` instead of the previously separate `byte`, `short`, `int`, `long`, `float`, `double` methods. (These otherwise harmless API changes improve consistency across numerical types.) Starting with version __1.16__, we also started deprecating some of the older API, either because methods were ill-conceived, confusing, or generally unsafe to use; or because they were internals of the library that should never have been exposed to users in the first place. Rest assured, the deprecations do not cripple the intended functionality of the library. If anything they make the library less confusing and safer to use. The Javadoc API documentation mentions alternatives for the methods that were deprecated, as appropriate. And, if nothing else works, you should still be able to compile your old code with deprecations enabled in the compiler options. Rest assured, all deprecated methods, no matter how ill-conceived or dodgy they may be, will be supported in all future releases prior to version __2.0__ of the library. ----------------------------------------------------------------------------- ## Reading FITS files - [FITS vs Java bytes](#fits-vs-java-bytes) - [Deferred reading](#deferred-reading) - [Tolerance to standard violations in 3rd party FITS files](#read-tolerance) - [Reading images](#reading-images) - [Reading tables](#reading-tables) ### FITS vs Java bytes Java bytes are signed, but FITS bytes are not. If any arithmetic processing is to be done on byte-valued data, users may need to be careful of Java’s automated conversion of signed bytes to widened integers. Whereas, a value of `0xFF` signifies 255 in FITS, it has a Java value of -1. To preserve the FITS meaning, you may want to upconvert FITS bytes to Java `short` values as: ```java short shortValue = (byteValue & 0xFF); ``` ### Deferred reading When FITS data are being read from a non-compressed random accessible input (such as a `FitsFile`), the `read()` call will parse all HDU headers but will typically skip over the data segments (noting their position in the file however). Only when the user tries to access data from an HDU, will the library load that data from the previously noted file position. The behavior allows to inspect the contents of a FITS file very quickly even when the file is large, and reduces the need for IO when only parts of the whole are of interest to the user. Deferred input, however, is not possible when the input is compressed or if it is uses a stream rather than a random-access `FitsFile`. One thing to keep in mind with deferred reading is that you should not close your `Fits` or its random-accessible input file before all the required data has been loaded. For example, the following will cause an error: ```java Fits fits = new Fits("somedata.fits"); // Scans the FITS, but defers loading data until we need it fits.read(); // We close the FITS prematurely. fits.close(); // !!!BAD!!! now if we try to access data // we'll get and exception... float[][] image = (float[][]) fits.getHDU(0).getKernel(); ``` In the above, the `getKernel()` method will try to load the deferred data from the input that we closed just before it. That's not going to work. The correct order is of course: ```java // Scans the FITS, but defers loading data until we need it fits.read(); // Good, the FITS is still open so we can get the deferred data float[][] image = (float[][]) fits.getHDU(0).getKernel(); // We close only after we grabbed all the data we needed. fits.close(); ``` As of version __1.18__, all data classes of the library support deferred reading. ### Tolerance to standard violations in 3rd party FITS files. By default the library will be tolerant to FITS standard violations when parsing 3rd-party FITS files. We believe that if you use this library to read a FITS produced by other software, you are mainly interested to find out what's inside it, rather than know if it was written properly. However, problems such as missing padding at the end of the file, or an unexpected end-of-file before content was fully parsed, will be logged so they can be inspected. Soft violations of header standards (those that can be overcome with educated guesses) are also tolerated when reading, but logging for these is not enabled by default (since they may be many, and likely you don't care). You can enable logging standard violations in 3rd-party headers by `Header.setParserWarningsEnabled(true)`. You can also enforce stricter compliance to the standard when reading FITS files via `FitsFactory.setAllowHeaderRepairs(false)` and `FitsFactory.setAllowTerminalJunk(false)`. When violations are not tolerated, appropriate exceptions will be thrown during reading. ### Reading Images - [Reading whole images](#reading-whole-images) - [Reading selected parts of images only (cutouts)](#reading-cutouts) - [Streaming image cutouts](#streaming-cutouts) - [Low-level reading of image data](#low-level-image-read) #### Reading whole images The simplest example of reading an image contained in the first HDU is given below: ```java Fits f = new Fits("myfile.fits"); ImageHDU hdu = (ImageHDU) f.readHDU(); int[][] image = (int[][]) hdu.getKernel(); ``` First we create a new instance of `Fits` with the filename. Then we can get the first HDU using the `getHDU()` method. Note the casting into an `ImageHDU`. When reading FITS data using the nom.tam library the user will often need to cast the results to the appropriate type. Given that the FITS file may contain many different kinds of data and that Java provides us with no class that can point to different kinds of primitive arrays other than `Object`, such explicit casting is inevitable if you want to use the data from the FITS files. #### Converting images to a different type As of version 1.20, the library provides the means for converting images between numerical types. Let's say the FITS stored the image as integers, but we want them as double-precision values. ```java double[][] darray = (double[][]) hdu.getData().convertTo(double.class).getKernel(); ``` Things get somewhat interesting at this point, because the FITS standard also allows for the integer representation of floating-point values, via quantization. The quantization has the following parameters: - A scaling factor, i.e. the separation of discrete levels in the floating point data (defaults to 1.0) - an offset, i.e. the floating point value that corresponds to an integer value of 0 (defaults to 0.0). - a blanking value, i.e. the integer that corresponds to NaN (not used by default). The quantization of images is defined by the `BSCALE`, `BZERO`, and `BLANK` keywords accordingly in the FITS header. Thus, when these keywords are present, the integer to decimal conversion will automatically apply the integer-to-decimal conversion as: ``` = * + ``` or else set NaN values when the integer value matches the designated blanking value. In the absence of the above header keywords defining quantization, the integer-to-decimal conversion is effectively just a widening conversion. The reverse conversion, from decimal images to integer images, involves rounding, i.e.: ``` = round(( - ) / ) ``` If you want to convert the image without quantization, you may call `ImageData.setQuantizer(null)` prior to the conversions, or else use the static low-level `ArrayFuncs.convertArray(Object, Class)` method instead. #### Complex valued images While the original FITS standard designated images for scalar numerical types only, the current standard (version 4.0) also specifies a convention to represent complex valued data as images. Complex arrays are recorded as any scalar numerical type, but with an extra dimension of 2 containing the real and imaginary components. E.g. a 4x3 complex array can thus be represented as any primitive type array with 4x3x2 dimensions. The convention is that the axis containing the complex pair of values has is `CTYPEn` header keyword named as 'COMPLEX'. Note, that complex values can be recorded as integers also, and use quantization just like decimals. As of version 1.20, this library recognizes when the convention is used when reading FITS image HDUs. (The library can only handle the convention if the 'COMPLEX' axis is the first or last image axis -- which should cover all but some very pathological use cases.) However, complex-valued images will continue to read back as their raw storage type (e.g. `float[4][3][2]`) for back compatibility with previous releases. But, you can easily check if the data is meant to be complex (or not) and convert them to complex values in a second step after loading the data. E.g.: ```java // Get the data in the stored data format as some primitive array... ImageData data = hdu.getData(); // If the complex array convention was used in the header, we can convert the image data // to complex-valued as the second step... if (data.isComplexValued()) { ComplexValue[][] z = (ComplexValue[][]) data.convertTo(ComplexValue.class).getKernel(); ... } ``` #### Reading selected parts of an image only (cutouts) Since version __1.18__, it is possible to read select cutouts of large images, including sparse sampling of specific image regions. When reading image data users may not want to read an entire array especially if the data is very large. An `ImageTiler` can be used to read in only a portion of an array. The user can specify a box (or a sequence of boxes) within the image and extract the desired subsets. `ImageTiler` can be used for any image. The library will try to only read the subsets requested if the FITS data is being read from an uncompressed file but in many cases it will need to read in the entire image before subsetting. Suppose the image we retrieve above has 2000x2000 pixels, but we only want to see the innermost 100x100 pixels. This can be achieved with ```java ImageTiler tiler = hdu.getTiler(); short[] center = (short[]) tiler.getTile(new int[] {950, 950}, new int[] {100, 100}); ``` The tiler needs to know the corners and size of the tile we want. Note that we can tile an image of any dimensionality. `getTile()` returns a one-dimensional array with the flattened 1D image. You can convert it to a 2D image afterwards using `ArrayFuncs.curl()`, e.g.: ```java short[][] center2D = (short[][]) ArrayFuncs.curl(center, 100, 100); ``` And you can convert to other numerical types, e.g. via one of the `ArrayFuncs.convertArray()` methods, e.g.: ``` double[][] dCenter2D = (double[][]) ArrayFuncs.convertArray(center2D, double.class, hdu.getData().getQuantizer()); ``` #### Streaming image cutouts Since version __1.18__ it is also possible to stream cutouts, using the `StreamingTileImageData` class. The streaming can be used with any source that implements the `RandomAccessFileIO` interface, which provides file-like random access, for example for a resource on the Amazon S3 cloud: Maven (`pom.xml`) dependency: ```xml software.amazon.nio.s3 aws-java-nio-spi-for-s3 2.2.0 ``` Gradle (`build.gradle`) dependency: ```groovy // https://mvnrepository.com/artifact/software.amazon.nio.s3/aws-java-nio-spi-for-s3 implementation 'software.amazon.nio.s3:aws-java-nio-spi-for-s3:2.2.0' ``` ```java Fits source = new Fits(new RandomChannelFileIO(...)); ImageHDU imageHDU = source.getHDU(...); // Manually set up the header for the cutout image as necessary Header cutoutHeader = ... // Define the image cutout region we want int[] tileStarts, tileLengths, tileSteps; ... // Create the cutout with the specified parameters StreamingTileImageData streamingTileImageData = new StreamingTileImageData( cutoutHeader, imageHDU.getTiler(), tileStarts, tileLengths, tileSteps ); // Add the cutout region to a new FITS object Fits output = new Fits(); output.addHDU(FitsFactory.hduFactory(cutoutHeader, streamingTileImageData)); // The cutout is processed at write time! output.write(outputStream); ``` As of version __1.18__ it is also possible to stream cutouts from compressed images using the `CompressedImageTiler` class. Whereas the `asImageHDU()` method decompresses the entire image in memory, the `CompressedImageTiler` will decompress only the tiles necessary for obtaining the desired cutout. For example, consider writing the cutout from a compressed image as a regular non-compressed `ImageHDU`. This can be achieved much the same way as in the above example, replacing `imageHDU.getTiler()` with a `CompressedImageTiler` step, such as: ```java ... CompressedImageTiler compressedImageTiler = new CompressedImageTiler(compressedImageHDU); StreamingTileImageData streamingTileImageData = new StreamingTileImageData( cutoutHeader, compressedImageTiler, corners, lengths, steps ); ... ``` #### Low-level reading of image data Suppose we want to get the average value of a 100,000 x 40,000 pixel image. If the pixels are 32-bit integers, that would be an 16 GB file. However, we do not need to load the entire image into memory at once. Instead we can analyze it via bite-sized chunks. For example, we start by finding the beginning of the relevant data segment in the file: ```java Fits fits = new Fits("bigimg.fits"); ImageHDU img = fits.getHDU(0); // Rewind the stream to the beginning of the data segment if (!img.getData().reset()) { // Uh-oh... throw new IllegalStateException("Unable to seek to data start”); } ``` The `reset()` method causes the internal stream to seek to the beginning of the data area. If that’s not possible it returns `false`. Next, we obtain the input file or stream for reading, query the image size, and set up our chunk-sized storage (e.g. by image row): ```java // Get the input associated to the FITS ArrayDataInput in = fits.getStream(); int[] dims = img.getAxes(); // the image dimensions int[] chunk = new int[dims[1]]; // a buffer for a row of data ``` Now we can cycle through the image rows (or chunks) and collect the statistics as we go, e.g.: ```java long sum = 0; for (int row = 0; row < dims[0]; row++) { in.readLArrayFully(chunk); for (int i = 0; i < chunk.length; i++) { sum += line[i]; } } // Return the average value return (double) sum / (dims[0] * dims[1]); ``` ### Reading Tables The easiest and safest way to access data in tables, is by individual entries. Typically, we start by identifying our table HDU in the FITS: ```java Fits f = new Fits("mytable.fits"); // Say, our table is the first extension HDU... TableHDU hdu = (TableHDU) f.getHDU(1); ``` If we are using a random-accessible input (like the file above), we have the option (for binary tables) to load the entire table into memory first. This may be a good idea for small tables, and/or if we plan to access all the data contained in the table -- or not such a good idea if we deal with huge tables from which we need only a selection of the entries. To load the entire HDU into memory: ```java // This will load the main table and the heap area into memory (if we want to...) hdu.getKernel(); ``` Next, we might want to find which columns store the data we need, using column names if appropriate. (We can of course rely on hard-coded column indices too when we know we are dealing with tables of known fixed format). ```java // Find column indices by name and check that they exist... int colUTC = hdu.findColumn("UTC"); if (colUTC < 0) { // uh-oh, there is no such column... } ``` Now we can loop through the rows of interest and pick out the entries we need. For example, to loop through all table rows to get only the scalar values from the column named `UTC` (see above), a phase value in the 4th column (Java index 3), and a spectrum stored in the fifth column (i.e. Java index 4): ```java // Loop through rows, accessing the relevant column data for(int row = 0; row < tab.getNRows(); row++) { // Retrieve scalar entries with convenient getters... double utc = tab.getDouble(row, colUTC); // We can also access by fixed column index... ComplexValue phase = (ComplexValue) tab.get(row, 3); ComplexValue[] spectrum = (ComplexValue[]) tab.get(row, 4); // process the data... ... } ``` The old `getElement()` / `setElement()` methods supported access as arrays only. While this is still a viable alternative (though slightly less elegant), we recommend against it going forward. Nevertheless, the equivalent to the above using this approach would be: ```java // Loop through rows, accessing the relevant column data for(int row = 0; row < tab.getNRows(); row++) { // Retrieve scalar entries by casting the element to the correct array // type, and returning the first (and only) element from that array... double utc = ((double[]) tab.getElement(row, colUTC))[0]; // We can also access by fixed column index... float[] phase = ((float[]) tab.getElement(row, 3)); float[][] spectrum = (float[][]) tab.getElement(row, 4); // process the data... ... } ``` These older methods (`getElement()`, `getRow()` and `getColumn()`) always return table data as arrays, even for scalar types, so a single integer entry will be returned as `int[1]`, a single string as `String[1]`. Complex values are stored as `float[2]` or `double[2]` depending on the precision (FITS type `C` or `M`). So, a double-precision FITS complex array of size `[5][7]` will be returned a `double[5][7][2]`. Logicals return `boolean[]`, which means that while FITS supports `null` logical values, we don't and these will default to `false`. (However, the `get()` method introduced in version __1.18__ will return these as `Boolean` arrays instead, retaining `null` values appropriately!). Note that for best performance you should access elements in monotonically increasing order when in deferred mode -- at least for the rows, but it does not hurt to follow the same principle for columns inside the loops also. This will help avoid excess buffering that way be required at times for backward jumps. The library provides methods for accessing entire rows and columns also via the `TableData.getRow(int)` and `TableData.getColumn(int)` or `BinaryTable.getColumn(String)` methods. However, we recommend against using these going forward because these methods return data that may be confounding to interpret, with non-trivial data types and/or dimensions. #### Converting array elements As of version 1.20, the library also support converting array elements to a different numerical type than the stored data. Like in the case for images (further above) the integer-decimal conversions will use the columns quantization parameters if they are defined. Otherwise narrowing conversions of decimal-to-integer types will use simple rounding. You can convert array elements via the `BinaryTable.getArrayElementAs()` method. E.g.: ```java BinaryTable tab = ...; // Assuming that the column contains 2D numerical entries of some type... // Get a table entry as an array of doubles, regardless of its (numerical) storage type... double[][] e = tab.getArrayElementAs(1, 3, double.class); ``` The quantization of columns is automatically digested based on the `TSCALn`, `TZEROn`, and `TNULLn` keywords in the table's header, but can be (re)set to a different quantization using the `ColumnDesc.setQuantizer()` method, if need be. Because binary tables already have designated complex-valued column types, the conversions apply only between scalar numerical types, such as `byte.class`, `short.class`, `int.class`, `long.class`, `float.class`, and `double.class`. ----------------------------------------------------------------------------- ## Writing FITS data - [Java strings vs FITS strings](#java-strings-vs-FITS-strings) - [Writing complete FITS files](#writing-files) - [Writing one HDU at a time](#incremental-writing) - [Low-level writes](#low-level-writes) ### Java strings vs FITS strings FITS generally represents character strings as byte arrays of ASCII characters, with legal values between `0x20` and `0x7E` (inclusive). The library automatically converts between Java `String`s and their FITS representations, by the appropriate narrowing conversion of 16-bit Unicode `char` to `byte`. Therefore, you should be careful to avoid using extended Unicode characters (and also ASCII beyond the `0x20` -- `0x7E` range) in `String`s, when including these in FITS. ### Writing complete FITS files When creating FITS files from data we have at hand, the easiest is to start with a `Fits` object. We can add to it image and/or table HDUs we create. When everything is assembled, we write the FITS to a file or stream: ```java Fits fits = new Fits(); fits.addHDU(...); ... fits.write("myfits.fits"); ``` Images can be added to the FITS at any point. For example, consider a 2D `float[][]` image we want to add to a FITS: ```java float[][] image ... ImageHDU imageHDU = Fits.makeHDU(image); fits.addHDU(imageHDU); ``` The `makeHDU()` method only populates the essential descriptions of the image in the HDU's header. We may want to complete that description (e.g. add WCS information, various other data descriptions) to the new HDU's header, e.g.: ```java Header header = imageHDU.getHeader(); header.addValue(Standard.BUNIT, "Jy/beam"); ... ``` After that we can add further images or table(s), such as binary tables (preferred) or ASCII tables. Once all HDUs have been assembled this way, we write the FITS as usual: ```java fits.write("myfits.fits"); fits.close(); ``` An important thing to remember is that while images can be anywhere in the FITS files, tables are extensions, and so, they may not reside in the first HDU in a file. Thus, if a table is the first HDU we add to a FITS container, it will be automatically prepended by a dummy primary HDU, and our data will actually be written as the second HDU (Java index 1). #### Binary versus ASCII tables When writing simple tables it may be possible to write the tables in either binary or ASCII format, provided all columns are scalar types. By default, the library will create and write binary tables for such data. To create ASCII tables instead the user should call `FitsFactory.setUseAsciiTables(true)` first. Given the superiority and compactness of binary tables, we recommend against using ASCII tables, unless you have to for a compelling reason. ### Writing one HDU at a time Sometimes you do not want to add all your HDUs to a `Fits` object before writing them out to a file or stream. Maybe because they use up too much RAM, or you are recording from a live stream and want to add HDUs to the file as they come in. As of version __1.17__ of the library, you can write FITS files one HDU at a time without having to place them in a `Fits` container first, or having to worry about the mandatory keywords having been set for primary or extension HDUs. Or, you can write a `Fits` object with some number of HDUs, but then keep appending further HDUs after, worry-free. The `FitsFile` or `FitsOutputStream` object will keep track of where things go in the file or stream, and set the required header keywords for the appended HDUs as appropriate for a primary or extension HDU automatically. Here is an example of how to create a FITS file HDU-by-HDU without the need for a `Fits` object as a holding container: ```java // Create the file to which to write the HDUs as they come FitsFile out = new FitsFile("my-incremental.fits", "rw"); ... // you can append 'hdu' objects to the FITS file (stream) as: // The first HDU will be set primary (if possible), and following HDUs will be extensions. hdu.write(out); ... // When you are all done you can close the FITS file/stream out.close(); ``` In the above case the `FitsFile` output is random accessible, which means you can go back and re-write HDUs (or their headers) in place later. If you do go all the way back to the head of the file, and re-write the first HDU, you can be assured that it will contain the necessary header entries for a primary HDU, even if you did not set them yourself. Easy as pie. Of course, you can use a `FitsOutputStream` as opposed to a file as the output also, e.g.: ```java FitsOutputStream out = new FitsOutputStream(new FileOutputStream("my-incremental.fits")); ... ``` in which case going back to re-write what was already written before is not an option. ### Low-level writes When a large table or image is to be written, the user may wish to stream the write. This is possible but rather more difficult than in the case of reads. There are two main issues: 1. The header for the HDU must written to show the size of the entire file when we are done. Thus the user may need to modify the header data appropriately. 2. After writing the data, a valid FITS file may need to be padded to an appropriate length. It's not hard to address these requirements, but the user needs some familiarity with the internals of the FITS representation. #### Images We can write images one subarray at a time, if we want to. Here is an example of how you could go about it. First, create storage for the contiguous chunk we want to write at a time. For example, same we want to write a 32-bit floating-point image with `[nRows][nCols]` pixels, and we want to write these one row at a time: First let's create storage for the chunk: ```java // An array to hold data for a chunk of the image... float[] chunk = new float[nCols]; ``` Next create a header. It's easiest to create it from the chunk, and then just modify the dimensions for the full image, e.g. as: ```java // Create an image HDU with the row BasicHDU hdu = Fits.makeHDU(row); Header header = hdu.getHeader(); // Override the image dimensions in the header to describe the full image ImageData.overrideHeaderAxes(header, nRow, nCol); ``` Next, we can complete the header description adding whatever information we desire. Once complete, we'll write the image header to the output: ```java // Create a FITS and write to the image to it FitsFile out = new FitsFile("image.fits", "rw"); header.write(out); ``` Now, we can start writing the image data, iterating over the rows, populating our chunk data in turn, and writing it out as we go. ```java // Iterate over the image rows for (int i = 0; i < nRows; i++) { // fill up the chunk with one row's worth of data ... // Write the row to the output out.writeArray(chunk); } ``` Finally, add the requisite padding to complete the FITS block of 2880 bytes after the end of the image data: ```java FitsUtil.pad(out, out.position()); out.close(); ``` #### Tables We can do something pretty similar for tables _so long as we don't have variable length columns_, but it requires a little more work. First we have to make sure we are not trying to write tables into the primary HDU of a FITS. Tables can only reside in extensions, and so we might need to create and write a dummy primary HDU to the FITS before we can write the table itself: ```java FitsFile out = new FitsFile("table.fits", "rw"); // Binary tables cannot be in the primary HDU of a FITS file // So we must add a dummy primary HDU to the FITS first if necessary new NullDataHDU().write(out); ``` Next, assume we have a binary table that we either read from an input, or else assembled ourselves (see further below on how to build binary tables): ```java BinaryTable table = ... ``` Next, we will need to create an appropriate FITS header for the table: ```java Header header = new Header(); table.fillHeader(header); ``` We can now complete the header description as we see fit, with whatever optional entries. We can also save space for future additions, e.g. for values we will have only after we start writing the table data itself: ```java // Make space for at least 200 more header lines to be added later header.ensureCardSpace(200); ``` Now, we can write out the header: ```java header.write(out); ``` Next, we can finally write regular table rows (without variable-length entries) in a loop. Assuming that our row is something like `{ { double[1] }, { byte[10] }, { float[256] }, ... }`: ```java for (...) { // Write data one element at the time into the buffer via the // rowStream. These must match the column structure of the table, // in terms of order, data types, and element counts. out.writeDouble(ra); out.write(fixedLengthNameBytes); out.witeArray(spectrum); ... } ``` We want to keep count of the rows we write (e.g. `nRowsWritten`). Once we finish writing the table data, we must add the requisite padding to complete the FITS block of 2880 bytes after the table data ends. ```java // Add padding to the file to complete the FITS block FitsUtil.pad(out, nRowsWritten * table.getRegularRowSize()); ``` After the table has been thus written to the output, we should make sure that the header has the correct number of table rows in in `NAXIS2` entry: ```java header.addValue(Standard.NAXISn.n(2), nRowsWritten); ``` We can also complete the header with any other information that became available since the start (using the space we reserved for additions earlier). Once the header is all in ship-shape, we can re-write in the file at its original location: ```java // Re-write the header with the new information we added since we began writing // the table data header.rewrite(); ``` ----------------------------------------------------------------------------- ## Modifying existing FITS files An existing FITS file can be modified in place in some circumstances. The file must be an uncompressed (random-accessible) file, with permissions to read and write. The user can then modify elements either by directly modifying the kernel data object for image data, or by using the `setElement` or similar methods for tables. Suppose we have just a couple of specific elements we know we need to change in a given file: ```java Fits f = new Fits("mod.fits"); ImageHDU hdu = (ImageHDU) f.getHDU(0); int[][] img = (int[][]) hdu.getKernel(); // modify the image as needed... img[i][j] = ... ... // write the new data back in the place of the old hdu.rewrite(); ``` Same goes for a table HDU: ```java BinaryTableHDU hdu = (BinaryTableHDU) f.getHDU(1); // Modify the table as necessary hdu.set(3, 0, 3.14159265); ... // Make sure the file contains the changes made above hdu.rewrite(); ``` Note, that in the above table example, the `rewrite()` call may be superfluous since `BinaryTable.set()` may be editing the file in situ if the data has been left in deferred-read mode (random accessible file, without data loaded to memory). Nevertheless, it is best practice to call `rewrite()` anyway to ensure that the updates are synced to the output under all circumstances. And, you should also close the output (e.g. via `Fits.close()`) after done editing the FITS file to ensure that any pending file changes are fully flushed to the output. Defragmenting binary tables allows to reclaim heap space that is no longer used in the heap area. When deleting variable-length columns, or when replacing entries inside variable-length columns, some or all of the space occupied by old entries on the heap may become orphaned, needlessly bloating the heap storage. Also, changed entries may be placed on the heap out of order, which can slow down caching effectiveness for sequential table access. Thus when modifying tables with variable-length columns, it may be a good idea to defragment the heap before writing in to the output. For the above example, this would be adding an extra step before `rewrite)`. ```java ... // If we changed variable-length data, it may be a good // idea to defragment the heap before writing... hdu.defragment(); hdu.rewrite(); ``` Defragmenting might also be a good idea when building tables with variable-length data column by column (as opposed to row-by-row). And, headers can also be updated in place also -- you don't even need to access the data, which can be left in deferred state: ```java BasicHDU hdu = f.getHDU(1); Header header = hdu.getHeader(); header.addValue(Standard.TELESCOP, "SMA").comment("The Submillimeter Array"); header.addValue(Standard.DATE-OBS, FitsDate.now()); ... header.rewrite(); ``` Generally rewrites can be made as long as the only change is to the data content, but not to the data size (and the FITS file meets the criteria mentioned above). An exception will be thrown if the data has been added or deleted or too many changes have been made to the header. Some additions to the header may be allowed as long as the header still fits in the same number of FITS blocks (of 2880 bytes) as before. (Hint, you can always reserve space in headers for later additions using `Header.ensureCardSpace(int)` prior to writing the header or HDU originally.) ----------------------------------------------------------------------------- ## FITS headers - [What is in a header](#what-is-in-a-header) - [Accessing header values](#accessing-header-values) - [Standard and conventional FITS header keywords](#standard-and-conventional-fits-header-keywords) - [Hierarchical and long header keywords](#hierarch-style-header-keywords) - [Long string values](#long-string-values) - [Checksums](#checksums) - [Preallocated header space](#preallocated-header-space) - [Standard compliance](#standard-compliance) - [Migrating header data between HDUs](#migrating-headers) ### What is in a header The FITS header consists of a list of 80-byte records at the beginning of each HDU. They contain key/value pairs and comments and serve three distinct purposes: 1. First and foremost, the header provides an _essential_ description of the HDU's data segment with a set of reserved FITS keywords and associated values. These _must_ appear in a specific place and order order in all FITS headers. The keywords `SIMPLE` or `XTENSION`, `BITPIX`, `NAXIS`, `NAXISn`, `PCOUNT`, `GCOUNT`, `GROUPS`, `THEAP`, `TFIELDS`, `TTYPEn`, `TBCOLn`, `TFORMn`, and `END` form the set of essential keywords. The library automatically takes care of adding these header entries in the required order, and users of the library should never attempt to set or modify the essential data description manually. 2. [FITS standard](https://fits.gsfc.nasa.gov/fits_standard.html) also reserves further header keywords to provide _optional_ standardized descriptions of the data, such as HDU names or versions, physical units, [World Coordinate Systems (WCS)](https://fits.gsfc.nasa.gov/fits_wcs.html), column names etc. It is up to the user to familiarize themselves with the _standard_ keywords and their usage, and use these to describe their data as fully as appropriate, or to extract information from 3rd party FITS headers. 3. Finally, the FITS headers may also store a user _dictionary_ of key/value pairs and/or comments. You may store whatever further information you like (within the constraints of what FITS allows) as long as they stay clear of the set of reserved FITS keywords mentioned above. It is a bit unfortunate that FITS was designed to mix the essential, standard, and user-defined keys in a single shared space of the same FITS header. It is therefore best practice for all creators of FITS files to: - Avoid setting or modifying the essential data description (which could result in corrupted or unreadable FITS files). Let the library handle these appropriately. - Keep standard (reserved) keywords separated from user-defined keywords in the header if possible. It is recommended for users to add the standardized header entries first, and then add any/all user-defined entries after. It is also recommended that users add a comment line (or lines) in-between to clearly denote where the standard FITS description ends, and where the user dictionary begins after. - Use comment cards to make headers self explanatory and easy for other humans to understand and digest. The header is also in a sense the self-contained documentation of your FITS data. Note, that originally, header keywords were limited to a maximum of 8 upper-case alphanumeric characters (`A` to `Z` and `0` to `9`), plus hyphens (`-`) and underscores (`_`), and string values may not exceed 68 characters in length. However, the [HIERARCH keyword convention](https://fits.gsfc.nasa.gov/registry/hierarch_keyword.html) allows for longer and/or more extended set of keywords that may utilize the ASCII range from `0x21` through `0x7E`, and which can contain hierarchies. And string values of arbitrary length may be added to headers via the [CONTINUE long keyword convention](https://fits.gsfc.nasa.gov/registry/continue_keyword.html), which is now an integral part of the standard as of FITS version 4.0. See more about these conventions, and their usage within this library, further below. ### Accessing header entries There are two basic ways to access data contained in FITS headers: direct (by keyword) or ordered (iterator-based). #### A. Direct access header entries You can retrieve keyed values by their associated keyword from the header using the `get...Value()` methods. To set values use one of the `addValue(...)` methods. These methods define a standard dictionary lookup access to key/value pair stored in the FITS headers. For example, to find out the telescope or observatory was used to obtain the data you might want to know the value of the `TELESCOP` key. ```java Fits f = new Fits("img.fits") Header header = f.getHDU(0).getHeader(); String telescope = header.getStringValue("TELESCOP"); ``` Note, that as of version __1.19__ you might want to use one of the `Fits.getCompleteHeader(...)` methods when inspecting headers of HDUs stored in the FITS file, since the header returned by these methods would also contain keywords indirectly inherited from the primary HDU, when the `INHERIT` keywords is used and set to `T` (true) in the header of the specified HDU extension. You can also use `header.getStringValue(Standard.TELESCOPE)` instead of the string constant to retrieve the same value with less chance of a typo spoiling your intent. See more on the use of standard keywords in the section below). Or if we want to know the right ascension (R.A.) coordinate of the reference position in the image: ```java double ra = header.getDoubleValue("CRVAL1"); ``` or, equivalently ```java double ra = header.getDoubleValue(WCS.CRVALna.n(1)); ``` [Note, that the FITS [WCS convention](https://fits.gsfc.nasa.gov/fits_wcs.html) is being used here. For typical 2D images the reference coordinates are in the pair of keys, `CRVAL1` and `CRVAL2` and our example assumes an equatorial coordinate system.] To add or change the R.A. coordinate value, you can use: ```java header.addValue("CRVAL1", ra, "[deg] R.A. coordinate"); ``` or, similarly ```java header.addValue(WCS.CRVALna.n(1), ra); ``` The second argument is our new right ascension coordinate (in degrees). The third is a comment field that will also be written to that header in the space remaining. (When using the standard keyword, the entry is created with the the standard comment belonging to the keyword, and you may change that by adding `.comment(...)` if you want it to be something more specific). The `addValue(...)` methods will update existing matching header entries _in situ_ with the newly defined value and comment, while it will add/insert _new_ header entries at the current _mark_ position. By default, this means that new entries will be appended at the end of the header, unless you have called `Header.findCard(...)` earlier to change the _mark_ position at which new card are added to that immediately before the specified other card, or else you called `Header.seekHead()` to add new cards at the start of the (non-essential) header space. Note, that you can always restore the default behavior of adding new entries at the end by calling `Header.seekTail()`, if desired. (This may be a little confusing at first, but the origins of the position marking behavior go a long way back in the history of the library, and therefore it is here to stay until at least version __2.0__.) Note, that the _mark_ position also applies to adding comment cards via `Header.insertComment()`, `.insertHistory()`, `.insertCommentStyle()` and related methods. Thus, `Header.findCard()`, `.seekHead()` and/or `.seekTail()` methods will allow you to surgically control header order when adding new cards to headers using the direct access methods. Table HDUs may contain several standard keywords to describe individual columns, and the `TableHDU.setColumnMeta(...)` methods can help you add these optional descriptor for your data while keeping column-specific keywords organized into header blocks around the mandatory `TFORMn` keywords. Note, that the `.setColumnMeta(...)` methods also change the mark position at which new header entries are added. #### B. Iterator-based access of header values For ordered access of header values you can also use the `nom.tam.util.Cursor` interface to step through header cards in the order they are stored in the FITS. ```java Cursor c = header.iterator(); ``` returns a cursor object that points to the first card of the header. The `Cursor.prev()` and `.next()` methods allow to step through the header, and `.add()` and `.delete()` methods can add/remove records at specific locations. The methods of `HeaderCard` allow us to manipulate the contents of the current card as desired. Comment and history header cards can be created and added to the header, e.g. via `HeaderCard.createCommentCard()` or `.createHistoryCard()` respectively. Note, that the iterator-based approach is the only way to extract comment cards from a header (if you are so inclined), since dictionary lookup will not work for these -- as comment cards are by definition not key/value pairs. ### Standard and conventional FITS header keywords The [FITS standard](https://heasarc.gsfc.nasa.gov/docs/fcg/standard_dict.html) defines a set of reserved keywords. You can find a collection of these under the `nom.tam.fits.header` package: * `Standard` -- The core keywords of the FITS standard. * `WCS` -- Standard FITS Word coordinate system (WCS) keywords * `DateTime` -- Standard date-time related FITS keywords * `Compression` -- Standard keywords used for describing compressed data * `Checksum` -- Standard keywords used for data checksumming In addition to the keywords defined by the FITS standard, the library also recognizes further conventional and commonly used keyword, which as also collected in the `nom.tam.fits.header` package: * `HierarchicalGrouping` -- Keywords for the [Hierarchical Grouping Convention](https://fits.gsfc.nasa.gov/registry/grouping.html) * `NonStandard` -- A few commonly used and recognized keywords that are not strictly part of the FITS standard * `DataDescription` -- Conventional keywords for describing the data content * `InstrumentDescription` -- Conventional keywords for describing the instrumentation used for observing * `ObservationDescription` -- Commonly used keywords that describe the observation * `ObservationDurationDescription` -- Commonly used keywords for the timing of observations Finally, many organizations (or groups of organizations) have defined their own sets of FITS keywords. Some of these can be found under the `nom.tam.fits-header.extra` package, such as: * `CommonExt` -- Commonly used keywords in the amateur astronomy community * `CXCExt` -- [keywords defined for the Chandra X-ray Observatory](https://cxc.harvard.edu/contrib/arots/fits/content.txt) * `ESOExt` -- keywords specific to the ESO [DataInterface Control Document](https://archive.eso.org/cms/tools-documentation/dicb/ESO-044156_7_DataInterfaceControlDocument.pdf) * `NOAOExt` -- keywords used by the National Optical Astronomy Observatory (_no longer available since the IRAF project is no longer supported_) * `SBFitsExt` -- [Santa Barbara Instrument Group FITS Extension (SBFITSEXT)](https://diffractionlimited.com/wp-content/uploads/2016/11/sbfitsext_1r0.pdf), a.k.a. SBIG keywords. * `MaxImDLExt` -- [MaxIm DL Astronomy and Scientific Imaging Solutions](https://www.cyanogen.com/help/maximdl/FITS_File_Header_Definitions.htm) * `STScIExt` -- [keywords used by the Space Telescope Science Institute](https://outerspace.stsci.edu/display/MASTDOCS/Required+Metadata) You can use the standardized keywords contained in these enums to populate headers or access header values. For example, ```java hdr.addValue(Standard.INSTRUME, "My super-duper camera"); hdr.addValue(InstrumentDescription.FILTER, "Meade #25A Red"); ... ``` The advantage of using these standardized keywords, as opposed to strings, is that they help avoid keyword typos, since the compiler (or your IDE) will warn you if the keyword name is not recognized. Some keywords contain indices that must be specified via the `n()` method. You must specicify one integer (one-based index) for each 'n' appearing in the keyword name. For example, to set the value of the `WAT9_234` keyword to the string value of `"50"`: ```java hdr.addValue(NOAOExt.WATn_nnn.n(9, 2, 3, 4), "50"); ``` For best practice, try rely on the standard keywords, or those in registered conventions, when possible. #### Keyword checking Another advantage of using the standardized keywords implementing the `IFitsHeader` interface is that the library can check (since __1.19__) automatically that (_a_) the keyword is appropriate for the type of HDU it is used in, and (_b_) if the keyword is one of the essential keywords that should be set by the library alone without users tinkering with them. If the keyword should not be used in the header belonging to a specific type of HDU under the current checking policy, the library will throw an `IllegalArgumentException`. You can use `Header.setKeywordChecking()` to adjust the type of checking to be applied on a per header instance basis, or use the static `Header.setDefaultKeywordChecking()` to change the default policy for all newly created headers. The `Header.KeywordCheck` enum defines the following policies that may be used: - `NONE` -- no keyword checking will be applied. You can do whatever you want without consequences. This policy is the most backward compatible one, since we have not done checking before. - `DATA_TYPE` -- Checks that the keyword is supported by the data type that the header is meant to describe. This is the default policy since version __1.19__ of the library. - `STRICT` -- In addition to checking if the keyword is suitable for the data type, the library will also prevent users from setting essential keywords that really should be handled by the library alone (such as `SIMPLE` or `XTENSION`, `BITPIX`, `NAXIS` etc.). #### Value checking The standardized keywords that implement the `IFitsHeader` interface can also specify the type of acceptable values to use. As of __1.19__ we will throw an appropriate exception (`IllegalArgumentException` or `ValueTypeException`, depending on the method) if the user attempt to set a value of unsupported type. For example trying to set the value of the `Standard.TELESCOP` keyword (which expects a string value) to a boolean will throw an exception. The keyword checking policy can be adjusted via the `HeaderCard.setValueCheckingPolicy()` method. `HeaderCard.ValueCheck` defines the following policies: - `NONE` -- no value type checking will be performed. You can do whatever you want without consequences. This policy is the most backward compatible one, since we have not done checking before. - `LOGGING` -- Attempting to set values of the wrong type for `IFitsHeader` keywords will be allowed but a warning will be logged each time. - `EXCEPTION` -- Attempting to set values of the wrong type for `IFitsHeader` keywords will throw an appropriate exception, such as `ValueTypeException` or `IllegalArgumentException` depending on the method used. This is the default policy since version __1.19__ of the library. ### Hierarchical and long header keywords The standard FITS header keywords consists of maximum 8 upper case letters (`A` through `Z`) or numbers (`0` through `9`) and/or dashes (`-`) and underscores (`_`). The [HIERARCH keyword convention](https://fits.gsfc.nasa.gov/registry/hierarch_keyword.html) allows for storing longer and/or hierarchical sets of FITS keywords, and can support a somewhat more extended set of ASCII characters (in the range of `0x21` to `0x7E`). Support for HIERARCH-style keywords is enabled by default as of version __1.16__. HIERARCH support can be toggled if needed via `FitsFactory.setUseHierarch(boolean)`. By default, HIERARCH keywords are converted to upper-case only (__cfitsio__ convention), so ```java HeaderCard hc = new HeaderCard(Hierarch.key("my.lower.case.keyword[!]"), "value", "comment"); ``` will write the header entry to FITS as: ``` HIERARCH MY LOWER CASE KEYWORD[!] = 'value' / comment ``` You can use `FitsFactory.getHierarchFormater().setCaseSensitive(true)` to allow the use of lower-case characters, and hence enable case-sensitive keywords also. After the setting, the same card will be written to FITS as: ``` HIERARCH my lower case keyword[!] = 'value' / comment ``` You may note a few other properties of HIERARCH keywords as implemented by this library: 1. The case sensitive setting (above) also determines whether or not HIERARCH keywords are converted to upper-case upon parsing also. As such, the header entry in last example above may be referred as `HIERARCH.my.lower.case.keyword[!]` or as `HIERARCH.MY.LOWER.CASE.KEYWORD[!]` internally after parsing, depending on whether case-sensitive mode is enabled or not, respectively. 2. If `FitsFactory` has HIERARCH support disabled, any attempt to define a HIERARCH-style long keyword will throw a `HierarchNotEnabledException` runtime exception. (However, just `HIERARCH` by itself will still be allowed as a standard 8-character FITS keyword on its own). 3. The convention of the library is to refer to HIERARCH keywords internally as a dot-separated hierarchy, preceded by `HIERARCH.`, e.g. `HIERARCH.my.keyword`. (The static methods of the `Hierarch` class can make it easier to create such keywords). 4. The HIERARCH keywords may contain all printable standard ASCII characters that are allowed in FITS headers (`0x20` thru `0x7E`). As such, we take a liberal reading of the ESO convention, which designated only upper-case letters, numbers, dashes (`-`) and underscores (`_`). If you want to conform to the ESO convention more closely, you should avoid using characters outside of the set of the original convention. 5. The library adds a space between the keywords and the `=` sign, as prescribed by the __cfitsio__ convention. The original ESO convention does not require such a space (but certainly allows for it). We add the extra space to offer better compatibility with __cfitsio__. 6. The HIERARCH parsing is tolerant, and does not care about extra space (or spaces) between the hierarchical components or before `=`. It also recognizes `.` as a separator of hierarchy besides the conventional white space. As such the following may all appear in a FITS header to define the same two-component keyword: ``` HIERARCH MY KEYWORD ``` ``` HIERARCH MY.KEYWORD ``` ``` HIERARCH MY .. KEYWORD ``` ### Long string values The standard maximum length for string values in the header is 68 characters. As of FITS 4.0, the [CONTINUE long string convention](https://fits.gsfc.nasa.gov/registry/continue_keyword.html) is part of the standard. And, as of version __1.16__ of this library, it is supported by default. Support for long strings can be turned off (or on again) via `FitsFactory.setLongStringEnabled(boolean)` if necessary. If the settings is disabled, any attempt to set a header value to a string longer than the space available for it in a single 80-character header record will throw a `LongStringsNotEnabledException` runtime exception. ### Checksums Checksums can be added to (and updated in) the headers of HDUs, and can be used to check the integrity of the FITS data after. `Fits`, `BasicHDU`, and `Data` classes provide methods both for setting / updating or for verifying checksums. The checksums will be calculated directly from the file (as of __1.17__) for all data in deferred read mode. Thus, it is possible to checksum or verify huge FITS files without having to load large volumes of data into RAM at any point. You can set the checksums (`CHECKSUM` and `DATASUM` keywords) before you write the FITS or the HDU it to disk: ```java BasicHDU hdu; // ... prepare the HDU and header ... hdu.setChecksum(); hdu.write(...); ``` Or you can set checksums for all HDUs in your `Fits` in one go before writing the entire `Fits` object out to disk: ```java Fits fits; // ... Compose the FITS with the HDUs ... fits.setChecksum(); fits.write(...); ``` Then later, as of version __1.18.1__, you can verify the integrity of FITS files using the stored checksums (or data sums) just as easily too: ```java try (Fits fits = new Fits("huge-file.fits")) { fits.verifyIntegrity(); } catch (FitsIntegrityException e) { // Failed integrity check } catch (...) // some other error... } ``` The above will calculate checksums for each HDU directly from the file without reading the potentially large data into memory, and compare HDU checksums and/or data checksums to those stored in the FITS headers. The verification can also be performed on stream inputs but, unlike for files, data will be invariable loaded into memory (at least temporarily). You can also verify the integrity of HDUs or their data segments individually, via `BasicHDU.verifyIntegrity()` or `BasicHDU.verifyDataIntegrity()` calls on specific HDUs. Finally, you might want to update the checksums for a FITS you modify in place: ```java Fits fits = new Fits("my.fits"); // We'll modify the fist HDU... ImageHDU im = (ImageHDU) fits.readHDU(); float[][] data = (float[][]) im.getData(): // Offset the data by 1.12 for (int i = 0; i < data.length; i++) for (int j = 0; i < data[0].length; j++) data[i][j] += 1.12; // Calculate new checksums for the HDU im.setChecksum(); im.rewrite(); ``` Or, (re)calculate and set checksums for all HDUs in a FITS file, once again leaving deferred data in unloaded state and computing the checksums for these directly from disk: ```java Fits fits = new Fits("my.fits"); fits.setChecksum(); fits.rewrite(); ``` The above will work as expected provided the original FITS already had `CHECKSUM` and `DATASUM` keys in the HDUs, or else the headers had enough unused space for adding these without growing the size of the headers. If any of the headers or data in the `Fits` have changed size, the `Fits.rewrite()` call will throw a `FitsException` without modifying any of the records. In such cases You may proceed re-writing a selection of the HDUs, or else write the `Fits` to a new file with a different size. ### Preallocated header space Many FITS files are created by live-recording of data, e.g. from astronomical instruments. As such not all header values may be defined when one begins writing the data segment of the HDU that follows the header. For example, we do not know in advance how many rows the binary table will contain, which will depend on when the recording will stop. Other metadata may simply not be available until a later time. For this reason version 4.0 of the FITS standard has specified preallocating header space as some number of blank header records between the last defined header entry and the `END` keyword. As of version __1.16__, this library supports preallocated header space via `Header.ensureCardSpace(int)`, which can be used to ensure that the header can contain _at least_ the specified number of 80-character records when written to the output. (In reality it may accommodate somewhat more than that because of the required padding to multiples of 2880 bytes or 36 records -- and you can use `Header.getMinimumSize()` to find out just how many bytes are reserved / used by any header object at any point). Once the space has been reserved, the header can be written to the output, and one may begin recording data after it. The header can be completed later, up to the number of additional cards specified (and sometimes beyond), and the updated header can be rewritten place at a later time with the additional entries. For example, ```java FitsFile out = new FitsFile("mydata.fits", "rw"); Header h = new Header(); // We want to keep room for 200 80-character records in total // to be filled later h.ensureCardSpace(200); // We can now write the header, knowing we can fill it with up to // 200 records in total at a later point h.write(out); ``` Now you can proceed to recording the data, such as a binary table row-by-row. Once you are done with it, you can go back and make edits to the header, adding more header cards, in the space you reserved earlier, and rewrite the header in front of the data without issues: ``` java // Once the data has been recorded we can proceed to fill in // the additional header values, such as the end time of observation h.addValue("DATE-END", FitsDate.getFitsDateString(), "end of observation"); // And we can re-write the header in place h.rewrite(); ``` Preallocated header space is also preserved when reading the data in. When parsing headers trailing blank header records (before the `END` key) are counted as reserved card space. (Internal blank cards, between two regular keyword entries, are however preserved as blank comment cards and their space will not be reusable unless these cards are explicitly removed first). After reading a header with preallocated space, the user can add at least as many new cards into that header as trailing blank records were found, and still call `rewrite()` on that header without any problems. ### Standard compliance As of version __1.16__, the library offers a two-pronged approach to ensure header compliance to the [FITS standard](#https://fits.gsfc.nasa.gov/fits_standard.html). - First, we fully enforce the standards when creating FITS headers using this library, and we do it in a way that is compliant with earlier FITS specifications (prior to 4.0) also. We will prevent the creation of non-standard header entries (cards) by throwing appropriate runtime exceptions (such as `IllegalArgumentException`, `LongValueException`, `LongStringsNotEnabledException`, `HierarchNotEnabledException`) as soon as one attempts to set a header component that is not supported by FITS or by the set of standards selected in the current `FitsFactory` settings. - Second, we offer the choice between tolerant and strict interpretation of 3rd-party FITS headers when parsing these. In tolerant mode (default), the parser will do its best to overcome standard violations as much as possible, such that the header can be parsed as fully as possible, even if some entries may have malformed content. The user may enable `Header.setParserWarningsEnabled(true)` to log each violation detected by the parser as warnings, so these can be inspected if the user cares to know. Stricter parsing can be enabled by `FitsFactory.setAllowHeaderRepairs(false)`. In this mode, the parser will throw an exception when it encounters a severely corrupted header entry, such as a string value with no closing quote (`UnclosedQuoteException`) or a complex value without a closing bracket (`IllegalArgumentException`). Lesser violations can still be logged, the same way as in tolerant mode. Additionally, we provide `HeaderCard.sanitize(String)` method that the user can call to ensure that a Java `String` can be used in FITS headers. The method will replace illegal FITS characters (outside of the range of `0x20` thru `0x7E`) with `?`. ## Migrating header data between HDUs Sometimes we want to create a new HDU based on an existing HDU, such as a cropped image, or a table segment, in which we want to reuse much of the information contained in the original header. The best way to go about it is via the following steps: 1. Start by creating the new HDU from the data it will hold. It ensures that the new HDU will have the correct essential data description (type and size) in its header. 2. Merge distinct (non-conflicting) header entries from the original HDU into the header of the new HDU, using the `Header.mergeDistinct(Header source)` method. It will migrate the header entries from the original HDU to the new one, without overriding the proper essential data description. 3. Update the header entries as necessary, such as [WCS](https://fits.gsfc.nasa.gov/fits_wcs.html), in the new HDU. Pay attention to removing obsoleted entries also, such as descriptions of table columns that no longer exist in the new data. 4. If the header contains checksums, make sure you update these before writing the header or HDU to an output. For example: ```java // Some image HDU whose header we want to reuse for another... ImageHDU origHDU = ... // 1. create the new image HDU with the new data float[][] newImage = ... ImageHDU newHDU = ImageData.from(newImage).toHDU(); // 2. copy over non-conflicting header entries from the original Header newHeader = newHDU.getHeader(); newHeader.mergeDistinct(origHDU.getHeader()); // 3. Update the WCS for the cropped data... newHeader.addValue(Standard.CRPIXn.n(1), ...); ... // 4. Update checksums, if necessary newHDU.setChecksum(); ``` ----------------------------------------------------------------------------- ## Creating tables - [Building tables row-by-row](#building-by-row) - [Building tables column-by-column](#building-by-column) - [Creating ASCII tables (discouraged)](#creating-ascii-tables) ### Building tables row-by-row As of version __1.18__ building tables one row at a time is both easy and efficient -- and may be the least confusing way to get tables done right. (In prior releases, adding rows to existing tables was painfully slow, and much more constrained). You may want to start by defining the types and dimensions of the data (or whether variable-length) that will be contained in each table column: ```java BinaryTable table = new BinaryTable(); // A column containing 64-bit floating point scalar values, 1 per row... table.addColumn(ColumnDesc.createForScalars(double.class)); // A column containing 5x4 arrays of single-precision complex values... table.addColumn(ColumnDesc.createForArrays(ComplexValue.Float.class, 5, 4)); // A column containing Strings of variable length using 32-bit heap pointers... table.addColumn(ColumnDesc.createForVariableLength(String.class)); ... ``` Defining columns this way is not always necessary before adding rows to the table. However, it is necessary if you will have data that needs variable-length storage row-after-row; or if you want more control over specifics of the column format. As such, it is best practice to define the columns explicitly even if not strictly required for your particular application. Now you can populate the table with your data, one row at a time, using the `addRow()` method as many times over as necessary: ```java for (...) { // prepare the row data, making sure each row is compatible with prior rows... ... // Add the row to the table table.addRow(...); } ``` As of version __1.18__, you may use Java boxed types (as an alternative to primitive arrays-of-one) to specify primitive scalar table elements, including auto-boxing of literals or variables. You may also use _vararg_ syntax for adding rows if that is more convenient in your application. Thus, you may simply write: ```java table.addRowEntries(1, 3.14159265); ``` to add a row consisting of an 32-bit integer, a double-precision floating point value (presuming your table has those two types of columns). Prior to __1.18__, the same would have to have been written as: ```java table.addRow(new Object[] { new int[] {1}, new double[] {3.14159265} }; ``` Tables built entirely row-by-row are naturally defragmented, as long as they are not modified subsequently. Once the table is complete, you can make a HDU from it: ```java BinaryTableHDU hdu = table.toHDU(); ``` which will populate the header with the requisite entries that describe the table. You can then edit the new header to add any extra information (while being careful to not modify the essential table description). Note, that once the table is encompassed in an HDU, it is generally not safe to edit the table data, since the library has no foolproof way to keep the header description of the table perfectly in sync. Thus it is recommended that you create table HDUs only after the table data has been fully populated. A few rules to remember when building tables by rows: - All rows must contain the same number of entries (the number of columns) - Entries in the same column must match in their column type in every row. - Entries must be of the following supported types: * A supported Java type (`String` or `ComplexValue`), or * primitive arrays (e.g. `int[]`, `float[][]`), or * Arrays of `Boolean` (logicals), `String` or `ComplexValue` (such as `Boolean[][]` or `String[]`), or * Scalar primitives stored as arrays of 1 (e.g. `short[1]`). - If entries are multi-dimensional arrays, they must have the same dimensionality and shape in every row. (Otherwise, they will be stored as variable-length arrays in flattened 1D format, where the shape may be lost). - If entries are one-dimensional, they can vary in size from row to row freely. - Java `null` entries are allowed for `String` and `Boolean` (logical) types, but not for the other data types. (these will map to empty strings or _undefined_ logical values respectively) ### Building tables column-by-column Sometimes we might want to assemble a table from a selection of data which will readily constitute columns in the table. We can add these as columns to an existing table (empty or not) using the `BinaryTable.addColumn(Object)` method. For example, say we have two arrays, one a time-series of spectra, and a matching array of corresponding timestamps. We can create a table with these (or add them to an existing table with a matching number of rows) as: ```java double[] timestamps = new double[nRows]; ComplexValue[][] spectra = new ComplexValue[nRows][]; ... BinaryTable tab = new BinaryTable(); table.addColumn(timestamps); table.addColumn(spectra); ``` There are just a few rules to keep in mind when constructing tables in this way: - All columns added this way must contain the same number of elements (number of rows). - In column data, scalars entries are simply elements in a 1D primitive array (e.g. `double[]`), in which each element (e.g. a `double`) is the scalar value for a given row. (I.e. unlike in the row-major table format required to create entire tables at once, we do not have to wrap scalar values in self-contained arrays of 1) - Other than the above, the same rules apply as for creating HDUs row-by-row (above). - If setting complex columns with arrays of `float[2]` or `double[2]` (the old way), you will want to call `setComplexColumn(int)` afterwards for that column to make sure they are labeled properly in the FITS header (rather than as real-valued arrays of `float` or `double`). - Similarly, if adding arrays of `boolean` values, you might consider calling `convertToBits(int)` on that column for a more compact storage option of the `true`/`false` values, rather than as 1-byte FITS logicals (default). Defragmenting might also be a good idea before writing binary tables with variable-length data built column by column (as opposed to row-by-row): ```java table.defragment(); ``` before calling `write()` on the encompassing HDU. ### Creating ASCII tables (discouraged) While the library also supports ASCII tables for storing a more limited assortment of _scalar_ entries, binary tables should always be your choice for storing table data. ASCII tables are far less capable overall. And while they may be readable from a console without the need for other tools, there is no compelling reason for using ASCII tables today. Binary tables are simply better, because they: - Support arrays (including multidimensional and variable-length). - Support more data types (such as logical, and complex values). - Offer additional flexibility, such as variable sized and multi-dimensional array entries. - Take up less space on disk - Can be compressed to an even smaller size However, if you insist on creating ASCII tables (provided the data allows for it) you may: - Build them column by column using one of the `AsciiTable.addColumn(...)` method, or - Build all at once, from a set of readily available columns via `AsciiTable.fromColumnMajor(Object[])` (since __1.19__), or else - Set `FitsFactory.setUseAsciiTables(true)` prior to calling `Fits.makeHDU()` or one of the factory methods to encapsulate a column-major table data objects automatically as ASCII tables whenever it is possible. (Note that while the `AsciiTable` class also provides an `.addRow(Object[])` method, we strongly recommend against it because it is extremely inefficient, i.e. painfully slow). Either way, you should keep in mind the inherent limitations of ASCII tables: - Only scalar entries are allowed (no arrays whatsoever!) - Only `int`, `long`, `float`, `double` and `String` entries are supported. ----------------------------------------------------------------------------- ## Compression support - [File level compression](#file-compression) - [Image compression](#image-compression) - [Table compression](#table-compression) Starting with version __1.15__ we include support for compressing images and tables. The compression algorithms have been ported to Java from __cfitsio__ to provide a pure 100% Java implementation. However, versions prior to __1.19.1__ had a number of lingering compression related bugs of varying severity, which may have prevented reliable use. ### File level compression It is common practice to compress FITS files using __gzip__ (`.gz` extension) so they can be exchanged in a more compact form. Java 8+ supports the creation of gzipped FITS out of the box, by wrapping the file's output stream into a `GZIPOutputStream` or , such as: ```java Fits f = ... FitsOutputStream out = new FitsOutputStream(new GZIPOutputStream( new FileOutputStream(new File("mydata.fits.gz")))); f.write(out); ``` While we only support GZIP compression for writing compressed files (thanks to Java's support out of the box), we can read more compressed formats using Apaches commons-compress library. We support reading compressed files produced via __gzip__ (`.gz`), the Linux __compress__ tools (`.Z`), and via __bzip2__ (`.bz2`). The decompression happens automatically when we construct a `Fits` object with an input stream: ```java new FileInputStream compressedStream = new FileInputStream(new File("image.fits.bz2")); // The input stream will be filtered through a decompression algorithm // All read access to the FITS will pass through that decompression... Fits fits = new Fits(compressedStream); ... ``` ### Image compression Image compression and tiling are fully supported by __nom.tam.fits__ as of __1.18__, including images of any dimensionality and rectangular morphologies. (Releases between __1.15__ and __1.17__ had partial image compression support for 2D square images only, while some quantization support for compression was lacking prior to __1.18__). The tiling of non-2D images follows the [CFITSIO convention](https://heasarc.gsfc.nasa.gov/docs/software/fitsio/compression.html) with 2D tiles, where the tile size is set to 1 in the higher dimensions. Compressing an image HDU is typically a multi-step process: 1. Create a `CompressedImageHDU`, e.g. with `fromImageHDU(ImageHDU, int...)`: ```java ImageHDU image = ... CompressedImageHDU compressed = CompressedImageHDU.fromImageHDU(image, 60, 40); ``` 2. Set up the compression algorithm, including quantization (if desired) via `setCompressAlgorithm(String)` and `setQuantAlgorithm(String)`, and optionally the compression method used for preserving the blank values via `preserveNulls(String)`: ```java compressed.setCompressAlgorithm(Compression.ZCMPTYPE_RICE_1) .setQuantAlgorithm(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_1) .preserveNulls(Compression.ZCMPTYPE_HCOMPRESS_1); ``` 3. Set compression (and quantization) options, via calling on `getCompressOption(Class)`: ```java compressed.getCompressOption(RiceCompressOption.class).setBlockSize(32); compressed.getCompressOption(QuantizeOption.class).setBZero(3.0).setBScale(0.1).setBNull(-999); ``` 4. Finally, perform the actual compression via `compress()`: ```java compressed.compress(); ``` After the compression, the compressed image HDU can be handled just like any other HDU, and written to a file or stream, for example (just not as the first HDU in a FITS...). The reverse process is simply via the `asImageHDU()` method. E.g.: ```java CompressedImageHDU compressed = ... ImageHDU image = compressed.asImageHDU(); ``` When compressing or decompression images, all available CPUs are automatically utilized. #### Accessing image header values without decompressing: You don't need to decompress the image to see what the decompressed image header is. You can simply call `CompressedImageHDU.getImageHeader()` to peek into the reconstructed header of the image before it was compressed: ```java CompressedImageHDU compressed = ... Header imageHeader = compressed.getImageHeader(); ``` #### Accessing specific parts of a compressed image Often compressed images can be very large, and we are interested in specific areas of it only. As such, we do not want to decompress the entire image. In these cases we can use the `getTileHDU()` method of `CompressedImageHDU` class to decompress only the selected image area. As of version __1.18__, this is really easy also: ```java CompressedImageHDU compressed = ... int[] fromPixels = ... int[] cutoutSize = ... ImageHDU cutout = compressed.getTileHDU(fromPixels, cutoutSize); ``` ### Table compression Table compression is also supported in __nom.tam.fits__ from version __1.15__, and more completely since __1.19.1__. When compressing a table, the 'tiles' are sets of contiguous rows within a column. Each column may use a different compression algorithm. As for FITS version 4.0 only lossless compression is supported for tables, and hence only `GZIP_1`, `GZIP_2` (default), `RICE_1` (with default options), and `NOCOMPRESS` are admissible. The default compression is with `GZIP_2`, unless explicitly defined otherwise. Tile compression mimics image compression, and is typically a 2-step process: 1. Create a `CompressedTableHDU`, e.g. with `fromBinaryTableHDU(BinaryTableHDU, int, String...)`, using the specified number of table rows per compressed block, and compression algorithm(s), e.g. `RICE_1` for the first column, and `GZIP_2` for the rest: ```java BinaryTableHDU table = ... CompressedTableHDU compressed = CompressedTableHDU.fromBinaryTableHDU(table, 4, Compression.RICE_1); ``` 2. Perform the compression via `compress()`: ```java compressed.compress(); ``` The two step process (as opposed to a single-step one) was probably chosen because it mimics that of `CompressedImageHDU`, where further configuration steps may be inserted in-between (which might become possible in a future FITS standard). But, of course we can combine the steps into a single line: ```java CompressedTableHDU compressed = CompressedTableHDU.fromBinaryTableHDU(table, 4, Compression.RICE_1).compress(); ``` After the compression, the compressed table HDU can be handled just like any other HDU, and written to a file or stream, for example. The reverse process is simply via the `asBinaryTableHDU()` method. E.g.: ```java CompressedTableHDU compressed = ... BinaryTableHDU table = compressed.asBinaryTableHDU(); ``` #### Accessing image header values without decompressing You don't need to decompress the table to see what the decompressed table header is. You can simply call `CompressedTableHDU.getTableHeader()` to peek into the reconstructed header of the original table before it was compressed: ```java CompressedTableHDU compressed = ... Header origHeader = compressed.getTableHeader(); ``` #### Decompressing select parts of a compressed binary table Sometimes we are interested in a section of the compressed table only. As of version __1.18__, this is really easy also. If you just want to uncompress a range of the compressed tiles, you can ```java CompressedTableHDU compressed = ... TableHDU section = compressed.asTableHDU(fromTile, toTile); ``` The resulting HDU will contain all columns but on only the uncompressed rows for the selected tiles. And, if you want to surgically access a range of data from select columns (and tiles) only: ```java CompressedTableHDU compressed = ... Object[] colData = compressed.getColumnData(colIndex, fromTile, toTile); ``` The methods `CompressedTableHDU.getTileRows()` and `.getTileCount()` can be used to help determined which tile(s) to decompress to get access to specific table rows. #### Note on compressing variable-length arrays (VLAs) The compression of variable-length table columns is a fair bit more involved process than that for fixed-sized table entries, and we only started properly supporting it in __1.19.1__. When compressing/decompressing tables containing VLAs, you should be aware of the very limited interoperability with other tools, including (C)FITSIO and its `fpack` / `funpack` (more on these below). In fact, we are not aware of any tool other than __nom.tam.fits__ that offers a truly complete and accurate implementation of this part of the standard. Note, that the [(C)FITSIO](https://heasarc.gsfc.nasa.gov/fitsio/) implementation of VLA compression diverges from the documented standard (FITS 4.0 and the original Pence et al. 2013 convention) by storing the adjoined descriptors in reversed order, w.r.t. the standard, on the heap. Our understanding, based on communication with the maintainers of the FITS standard, is that this discrepancy will be resolved by changing the documentation (the standard) to conform to the (C)FITSIO implementation. Therefore, our implementation for the compression of VLAs is generally compliant to that of (C)FITSIO, and not to the current prescription of the standard. However, we wish to support reading files produced either way via the `static` `CompressedTableHDU.useOldStandardVLAIndexing(boolean)` method selecting the convention according to which the adjoined table descriptors are stored in the file: either in the format described by the original FITS 4.0 standard / Pence+2013 convention (`true`), or else in the (C)FITSIO compatible format (`false`; default). #### Interoperability with (C)FITSIO's `fpack` / `funpack` The table compression implementation of __nom.tam.fits__ is now both more standard(!) and more reliable(!) than that of (C)FITSIO and `fpack` / `funpack`. Issues of interoperability are not due to a fault of our own. Specifically, the current (4.5.0) and earlier [(C)FITSIO](https://heasarc.gsfc.nasa.gov/fitsio/) releases are affected by a slew of table-compression related bugs, quirks, and oddities -- which severely limit its interoperability with other tools. Some of the bugs in `fpack` may result in entirely corrupted FITS files, while others limit what standard compressed data `funpack` is able to decompress: 1. (C)FITSIO and `fpack` version <= 4.5.0 do not properly handle the `THEAP` keyword (if present). If the keyword is present, `fpack` will use it incorrectly, resulting in a bloated compressed FITS that is also unreadable because of an incorrect `PCOUNT` value in the compressed header. Therefore, we will skip adding `THEAP` to the table headers when not necessary (that is when the heap follows immediately after the main table), in order to provide better interoperability with (C)FITSIO and `fpack`. 2. (C)FITSIO and `fpack` version <= 4.5.0 do not handle `byte`-type and `short`-type VLA columns properly. In the `fpack`-compressed headers these are invariably indicated as compressed via `GZIP_1` and `GZIP_2` respectively (regardless of the user-specified compression option), whereas the data on the heap is not actually compressed for either. And while `fpack` does compress `int` type VLAs properly, it does it with `RICE_1` always, regardless of the user selection for the compression option. To a lesser extent, the `fpack` compression of fixed-sized columns is also lacking: fixed-sized `byte[]` and/or `short[]` array columns they are invariably compressed with `GZIP_1` and `GZIP_2`, respectively, as indicated (but still ignoring the user's choice for the requested compression type). 3. (C)FITSIO and `funpack` version <= 4.5.0 do not handle uncompressed data columns (with `ZCTYPn = 'NOCOMPRESS')` in compressed tables, despite these being standard. ----------------------------------------------------------------------------- ## Release schedule A predictable release schedule and process can help manage expectations and reduce stress on adopters and developers alike. Releases of the library follow a quarterly release schedule since version __1.16__. You may expect upcoming releases to be published around __March 15__, __June 15__, __September 15__, and/or __December 15__ each year, on an as needed basis. That means that if there are outstanding bugs, or new pull requests (PRs), you may expect a release that addresses these in the upcoming quarter. The dates are placeholders only, with no guarantee that a release will actually be available every quarter. If nothing of note comes up, a potential release date may pass without a release being published. _Feature releases_ (__1.x.0__ version bumps), which may include significant API changes, are provided at least 6 months apart, to reduce stress on adopters who may need/want to tweak their code to integrate these. Between feature releases, _bug fix releases_ (without significant API changes) may be provided as needed to address issues. New features are generally reserved for the feature releases, although they may also be rolled out in bug-fix releases as long as they do not affect the existing API -- in line with the desire to keep bug-fix releases fully backwards compatible with their parent versions. In the month(s) preceding releases one or more _release candidates_ (e.g. `1.19.1-rc3`) will be available on GitHub briefly, under [Releases](https://github.com/nom-tam-fits/nom-tam-fits/releases), so that changes can be tested by adopters before the releases are finalized. Please use due diligence to test such release candidates with your code when they become available to avoid unexpected surprises when the finalized release is published. Release candidates are typically available for one week only before they are superseded either by another, or by the finalized release. ----------------------------------------------------------------------------- ## How to contribute The _nom-tam-fits_ library is a community-maintained project. We absolutely rely on developers like you to make it better and to keep it going. Whether there is a nagging issue you would like to fix, or a new feature you'd like to see, you can make a difference yourself. We welcome you as a contributor. You became part of our community the moment you landed on this page. We very much encourage you to make this project a little bit your own, by submitting pull requests with fixes and enhancement. When you are ready, here are the typical steps for contributing to the project: 1. Old or new __Issue__? Whether you just found a bug, or you are missing a much needed feature, start by checking open (and closed) [Issues](https://github.com/nom-tam-fits/nom-tam-fits/issues). If an existing issue seems like a good match to yours, feel free to comment on it, and/or to offer help in resolving it. If you find no issues that match, go ahead and create a new one. 2. __Fork__. Is it something you'd like to help resolve? Great! You should start by creating your own fork of the repository so you can work freely on your solution. We also recommend that you place your work on a branch of your fork, which is named either after the issue number, e.g. `issue-192`, or some other descriptive name, such as `implement-foreign-hdu`. 3. __Develop__. Feel free to experiment on your fork/branch. If you run into a dead-end, you can always abandon it (which is why branches are great) and start anew. You can run your own test builds locally using `mvn clean test` before committing your changes. If the tests pass, you should also try running `mvn clean package` and `mvn site stage` to ensure that the package and Javadoc are also in order. Remember to synchronize your `master` branch by fetching changes from upstream every once in a while, and rebasing your development branch. Don't forget to: - Add __Javadoc__ your new code. You can keep it sweet and simple, but make sure it properly explains your methods, their arguments and return values, and why and what exceptions may be thrown. You should also cross-reference other methods that are similar, related, or relevant to what you just added. - Add __Unit Tests__. Make sure your new code has as close to full unit test coverage as possible. You should aim for 100% diff coverage. When pushing changes to your fork, you can get a coverage report by checking the GitHub Actions result of your commit (click the Codecov link), and you can analyze what line(s) of code need to have tests added. Try to create tests that are simple but meaningful (i.e. check for valid results, rather than just confirm existing behavior), and try to cover as many realistic scenarios as appropriate. Write lots of tests if you need to. It's OK to write 100 lines of test code for 5 lines of change. Go for it! And, you will get extra kudos for filling unit testing holes outside of your area of development! 4. __Pull Request__. Once you feel your work can be integrated, create a pull request from your fork/branch. You can do that easily from the GitHub page of your fork/branch directly. In the pull request, provide a concise description of what you added or changed. Your pull request will be reviewed. You may get some feedback at this point, and maybe there will be discussions about possible improvements or regressions etc. It's a good thing too, and your changes will likely end up with added polish as a result. You can be all the more proud of it in the end! 5. If all goes well, your pull-request will get merged, and will be included in the upcoming release of _nom-tam-fits_. Congratulations for your excellent work, and many thanks for dedicating some of your time for making this library a little bit better. There will be many who will appreciate it. :-) If at any point you have questions, or need feedback, don't be afraid to ask. You can put your questions into the issue you found or created, or your pull-request, or as a Q&A in [Discussions](https://github.com/nom-tam-fits/nom-tam-fits/discussions). nom-tam-fits-1.21.0/codecov.yml000066400000000000000000000006471476377620500163030ustar00rootroot00000000000000codecov: require_ci_to_pass: no coverage: precision: 2 round: down status: patch: off project: default: target: auto threshold: 0.03% removed_code_behavior: adjust_base parsers: gcov: branch_detection: conditional: yes loop: yes method: no macro: no comment: layout: "reach,diff,flags,files,footer" behavior: default require_changes: no nom-tam-fits-1.21.0/pom.xml000066400000000000000000000637561476377620500154650ustar00rootroot00000000000000 4.0.0 org.sonatype.oss oss-parent 7 gov.nasa.gsfc.heasarc nom-tam-fits 1.21.0 jar nom.tam.fits Java library for reading and writing FITS files. FITS, the Flexible Image Transport System, is commonly used for the archival and distribution of astronomical data. 1996 nom-tam-fits http://nom-tam-fits.github.io/nom-tam-fits attipaci Attila Kovacs attila [dot] kovacs [at] cfa [dot] harvard [dot] edu Maintainer Developer Admin http://1.gravatar.com/avatar/cc5454473320275f87bcae3c13aec99d https://www.sigmyne.com/attila at88mph Dustin Jenkins djenkins.cadc [at] gmail [dot] com Developer Admin https://avatars.githubusercontent.com/u/1671908?v=4 tmcglynn Thomas A Mcglynn Thomas.A.McGlynn [at] NASA [dot] gov Creator Admin ritchieGitHub Richard van Nieuwenhoven ritchie [at] gmx [dot] at Developer Admin http://www.gravatar.com/avatar/9e2c2e7aa94335b72952a4b2d56bfc89.png erikfk Erik Koerber erik.koerber [at] gmail [dot] com Developer Admin http://www.gravatar.com/avatar/6f82a608d349cc24f05604242422b1b8.png Kevin Eastridge William H. Cleveland Jr. whclevelandjr [at] gmail [dot] com David Glowacki R.J. Mathar Laurent Michel Guillaume Belanger Laurent Bourges Rose Early Jorgo Baker Vincenzo Forchi vforchi [at] eso [dot] org J.C. Segovia Booth Hartley Jason Weiss Martin Vrábel Jonathan Cook John Murphy scm:git:https://github.com/nom-tam-fits/nom-tam-fits.git scm:git:https://github.com/nom-tam-fits/nom-tam-fits.git https://github.com/nom-tam-fits/nom-tam-fits HEAD nexus i4j sonatype Snapshot Maven 2 repository https://oss.sonatype.org/content/repositories/snapshots github-project-site GitHub Maven 2 Project Site gitsite:git@github.com/nom-tam-fits/nom-tam-fits.git GitHub https://github.com/nom-tam-fits/nom-tam-fits/issues/ Public Domain file://${project.basedir}/src/license/publicdomain/license.txt repo org.apache.maven.wagon wagon-ssh 3.5.3 org.apache.maven.scm maven-scm-provider-gitexe 2.1.0 org.apache.maven.scm maven-scm-manager-plexus 2.1.0 com.github.stephenc.wagon wagon-gitsite 0.5 org.apache.maven.plugins maven-source-plugin 3.3.1 attach-sources jar-no-fork attach-test-sources test-jar-no-fork false org.apache.maven.plugins maven-deploy-plugin 3.1.4 org.apache.maven.plugins maven-resources-plugin 3.3.1 copy-docs pre-site copy-resources ${basedir}/src/site/markdown ${basedir} README.md CHANGELOG.md CONTRIBUTING.md org.apache.maven.plugins maven-gpg-plugin 3.2.7 gpg sign-artifacts verify sign org.apache.maven.plugins maven-compiler-plugin 3.14.0 8 org.apache.maven.plugins maven-javadoc-plugin 3.11.2 8 false true all,-missing attach-javadocs jar org.apache.maven.plugins maven-jar-plugin 3.4.2 org.apache.maven.plugins maven-release-plugin 3.1.1 forked-path false org.apache.maven.plugins maven-site-plugin 3.21.0 false org.apache.maven.wagon wagon-ssh 3.5.3 org.apache.maven.doxia doxia-module-markdown 2.0.0 org.codehaus.mojo license-maven-plugin 2.5.0 false publicdomain https://raw.githubusercontent.com/nom-tam-fits/nom-tam-fits/master/src/license first update-file-header update-project-license process-sources src/main/java src/test com.googlecode.maven-java-formatter-plugin maven-java-formatter-plugin 0.4 format ${project.basedir}/src/main/eclipse/formatter.xml org.apache.maven.plugins maven-checkstyle-plugin 3.6.0 com.puppycrawl.tools checkstyle 10.21.4 checkstyle process-sources ${project.basedir}/src/main/checkstyle/nom-tam-fits-style.xml ${project.basedir}/src/main/checkstyle/checkstyle-suppressions.xml checkstyle.suppressions.file true true org.jacoco jacoco-maven-plugin 0.8.12 default-prepare-agent prepare-agent default-report prepare-package report default-check check BUNDLE LINE COVEREDRATIO 0.98 org.eluder.coveralls coveralls-maven-plugin 4.3.0 javax.xml.bind jaxb-api 2.3.1 com.github.spotbugs spotbugs-maven-plugin 4.9.2.0 check package FindReturnRef,ConstructorThrow org.apache.maven.plugins maven-surefire-plugin 3.5.2 1.5C -Xmx2G @{argLine} src/test/resources/logging.properties ${skip.backbox.images} default-test nom.tam.fits.test.CompressWithoutDependenciesTest classpath-test test test nom.tam.fits.test.CompressWithoutDependenciesTest org.apache.commons:commons-compress org.simplify4u.plugins sitemapxml-maven-plugin 2.2.0 gen 10 org.apache.maven.plugins maven-project-info-reports-plugin 3.9.0 false false dependencies team issue-management licenses com.github.spotbugs spotbugs-maven-plugin 4.9.2.0 FindReturnRef,ConstructorThrow org.apache.maven.plugins maven-javadoc-plugin 3.11.2 8 false false ${project.name} ${project.version} Public User API ${project.name} ${project.version} Public User API astronomy software, Java library, FITS file, FITS format, datasets, data analysis, image processing
]]>
--allow-script-in-comments public nom.tam.fits.compress: nom.tam.fits.compression.algorithm.gzip: nom.tam.fits.compression.algorithm.gzip2: nom.tam.fits.compression.algorithm.plio: nom.tam.fits.compression.algorithm.uncompressed: nom.tam.fits.compression.provider: nom.tam.fits.compression.provider.param.*: nom.tam.image.compression.bintable: nom.tam.image.compression.tile: nom.tam.image.compression.tile.mask: nom.tam.image.tile.* **/CompressionLibLoaderProtection.java **/CloseIS.java **/ICompressor*.java **/HCompress.java **/HDecompress.java **/HCompressor.java **/RiceCompressor.java **/BitBuffer.java **/Quantize.java **/QuantizeProcessor.java **/FitsCopy.java **/FitsReader.java **/Main.java **/FitsLineAppender.java **/FitsSubstring.java **/AsciiTableData.java **/HeaderCommentsMap.java **/HeaderCardCountingArrayDataInput **/HeaderOrder.java **/StandardCommentReplacement.java
javadoc-no-fork
org.apache.maven.plugins maven-jxr-plugin 3.6.0 true
org.apache.commons commons-compress 1.27.1 true com.github.spotbugs spotbugs-annotations 4.9.2 compile org.junit.vintage junit-vintage-engine 5.12.0 test org.nanohttpd nanohttpd-webserver 2.3.1 test org.openjdk.jmh jmh-core 1.37 test org.openjdk.jmh jmh-generator-annprocess 1.37 test jakarta.annotation jakarta.annotation-api 3.0.0 compile UTF-8 nom-tam-fits-deploy nexus-repo true nexus i4j sonatype Maven 2 repository https://oss.sonatype.org/service/local/staging/deploy/maven2 github-repo github GitHub nom-tam-fits Apache Maven Packages https://maven.pkg.github.com/nom-tam-fits/nom-tam-fits skipBlackBoxImages true public-api org.apache.maven.plugins maven-javadoc-plugin 8 false false ${project.name} ${project.version} Public User API ${project.name} ${project.version} Public User API public
nom-tam-fits-1.21.0/release-HOWTO.md000066400000000000000000000141221476377620500167670ustar00rootroot00000000000000# Release HOWTO This document is meant for package maintainers only. Its purpose is to provide instructions on preparing and releasing updates to the __nom-tam-fits__ library. ## Prerequisites To release packages, you will need: 1. admin privileges on the mainline [nom-tam-fits](https://www.github.com/nom-tam-fits/nom-tam-fits) repo. 2. a Nexus Sonatype account. If not register one (see the [guide to publishing packages on Sonatype](https://central.sonatype.org/publish/publish-guide/)). We use the Sonatype Nexus repository for staging releases before pushing them to Maven Central, and also to provide automatic SNAPSHOT releases. (Releases, including SNAPSHOTs are pushed automatically to the Nexus repository by the GitHub Actions CI (`.github/workflows/maven.yml`). 3. push privileges to the `gov.nasa.gsfc.heasarc` repo on Nexus. If you don't already have them, one of the other nom-tam-fits maintainers can request it for you. (It may take up to 2 days after the request to gain access). 4. the `NEXUS_USERNAME` and `NEXUS_PASSWORD` repository secrets on github with your Nexus username and authentication token. At any point, there should be only one of the maintainers publishing packages to Nexus. So use your credentials only if you are that designated person. ## Preparing the release * Make sure the CI build on github passes without errors. If the CI build has issues, fix them before proceeding. * Check that unit tests coverage did not decrease compared to the prior release. (You should not merge pull request in the first place until they maintain or increase test coverage.) Small decrements in overall coverage due to a reduction in the total lines of code are acceptable, as long as the diff coverage itself is equal or above the previous coverage metric. (You should generally insist on 100% diff-coverage, unless there is a good reason why it cannot be attained.) * Clean up and format code, e.g. with Eclipse's Code Cleanup feature to ensure a degree of consistency across the source tree. Use the same formatting rules for subsequent releases. After the code has been cleaned/formatted, run `mvn clean test` to confirm viability before committing. * Edit `CHANGELOG.md` to summarize the changes for the release, linking entries to issues or pull request as appropriate. Commit and push the updates as necessary. Note any issues pertainig to compatibility at the top of the list of actions. * Update `pom.xml` with the latest (or best fit) plugin versions. Test them locally with `mvn clean package` before committing and pushing the POM to the repo. Alternatively, if dependabot is generating update PRs, make sure you integrate all that pass the CI. * Make sure the Project Site is in good shape. Run `mvn clean site`. The open `target/site/index.html` in a web browser. Click through the menu on the left panel and check that all content is current. Check that the changes are properly shown. Check that the _Getting Started_ guide has up-to-date instructions for using the library. ## Publishing the release Once you are confident that everything is in perfect order for the next release, change the version number in `pom.xml`. Remove `-SNAPHOT` from the version. The release needs a proper version number, such a `1.17.2` for finalized release, or something like `1.17.2-rc5` for release candidates and pre-releases. * Commit and push the updated `pom.xml` to the mainline master. Following a successful build, the Github Actions CI will upload the release artifacts to the Nexus staging repository. * Log into Sonatype Nexus ([oss.sonatype.org](https://oss.sonatype.org)) and click _Staging Repositories_ in the left menu panel. Your freshly packaged release should show up here. If you don't see it, it's either because Github Actions failed (or did not run at all), or because it uploaded to Nexus with someone else's credentials. If necessary, retrace your steps and fix what is needed to get the package published to Nexus staging with your credentials. * Sleep on it. So far so good, but this is also you last chance to fix anything before the package really goes public, so don't rush it. Take some time to reflect on it, double or triple-check everything, before moving to the next step... * On Github, click on _Releases_, and create a new release: - Name and tag the release with by the version, such as `1.17.0-rc1` (Note, before 1.17.0 the tag included a 'nom-tam-fits-' prefix as well, which resulted in source tarballs named as nom-tam-fits-nom-tam-fits- -- therefore starting with 1.17.0 we'll omit that). - Link the release to the last master commit of the repo. - If it is not a final release, be sure to check the box for _pre-release_ near the bottom (when checked the CI will not publish a Github package for this release). - Write up a summary of what's in the release. It can be a digested version of the changes, or some other concise summary. - Attach the signed package and javadoc JARs. (Best to use the signed JAR that has been uploaded to Nexus staging.) - After creating the release, delete any prior pre-releases (We should only track final releases in the long run). * After your upload to Sonatype Nexus, you need _Close_ the release, s.t. it cannot be modified further. (Prior to closing, in principle you could upload more artifacts from the same host as before -- but since we used the github CI to upload, there is little you can do to add anything really). Name the release with the version number, such as `1.17.0-rc4`. * Nexus will now give you options to _Release_ to Maven Central or _Drop_ it. If it is a final release, and you are ready to push it to Maven Central, then go ahead and click _Release_. Or, if it's a pre-release, you can drop it once it gets obsoleted (before then collaborators can access the pre-release on Nexus too, so do keep pre-releases around for them as long as it's appropriate). * Finally, edit `pom.xml`, and bump the version number and add `-SNAPSHOT`, for example: change the just released `1.17.0` to `1.17.1-SNAPSHOT`. I.e., from here on new commits on master will be part of the 1.17.1 development. Commit and push `pom.xml`. nom-tam-fits-1.21.0/settings.xml000066400000000000000000000005211476377620500165070ustar00rootroot00000000000000 nexus ${env.NEXUS_USERNAME} ${env.NEXUS_PASSWORD} github ${env.PKG_USERNAME} ${env.PKG_PASSWORD} nom-tam-fits-1.21.0/src/000077500000000000000000000000001476377620500147165ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/license/000077500000000000000000000000001476377620500163405ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/license/licenses.properties000066400000000000000000000000321476377620500222560ustar00rootroot00000000000000publicdomain=Public Domainnom-tam-fits-1.21.0/src/license/publicdomain/000077500000000000000000000000001476377620500210065ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/license/publicdomain/header.txt000066400000000000000000000021731476377620500230020ustar00rootroot00000000000000This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.nom-tam-fits-1.21.0/src/license/publicdomain/license.txt000066400000000000000000000023111476377620500231660ustar00rootroot00000000000000The Unlicense This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to nom-tam-fits-1.21.0/src/main/000077500000000000000000000000001476377620500156425ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/checkstyle/000077500000000000000000000000001476377620500200005ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/checkstyle/checkstyle-suppressions.xml000066400000000000000000000010521476377620500254310ustar00rootroot00000000000000 nom-tam-fits-1.21.0/src/main/checkstyle/nom-tam-fits-style.xml000066400000000000000000000141321476377620500241740ustar00rootroot00000000000000 nom-tam-fits-1.21.0/src/main/eclipse/000077500000000000000000000000001476377620500172665ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/eclipse/formatter.xml000066400000000000000000000750121476377620500220200ustar00rootroot00000000000000 nom-tam-fits-1.21.0/src/main/java/000077500000000000000000000000001476377620500165635ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/.classpath000066400000000000000000000004541476377620500205510ustar00rootroot00000000000000 nom-tam-fits-1.21.0/src/main/java/.gitignore000066400000000000000000000000061476377620500205470ustar00rootroot00000000000000/bin/ nom-tam-fits-1.21.0/src/main/java/.project000066400000000000000000000005601476377620500202330ustar00rootroot00000000000000 fits-main org.eclipse.jdt.core.javabuilder org.eclipse.jdt.core.javanature nom-tam-fits-1.21.0/src/main/java/nom/000077500000000000000000000000001476377620500173545ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/000077500000000000000000000000001476377620500201355ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/000077500000000000000000000000001476377620500211025ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/AbstractTableData.java000066400000000000000000000033031476377620500252510ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * Just an abstract class to reuse the myData in all subclasses of the HDU's. * * @author Richard van Nieuwenhoven * @deprecated Serves no purpose really. It can be removed in the future, * letting {@link AsciiTable} extend {@link Data} and implement * {@link TableData} instead. */ public abstract class AbstractTableData extends Data implements TableData { } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/AsciiTable.java000066400000000000000000001165121476377620500237530ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam.fits * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; import nom.tam.fits.header.Bitpix; import nom.tam.fits.header.IFitsHeader; import nom.tam.fits.header.Standard; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.ArrayFuncs; import nom.tam.util.ByteFormatter; import nom.tam.util.ByteParser; import nom.tam.util.Cursor; import nom.tam.util.FormatException; import static nom.tam.fits.header.Standard.NAXIS1; import static nom.tam.fits.header.Standard.NAXIS2; import static nom.tam.fits.header.Standard.TBCOLn; import static nom.tam.fits.header.Standard.TDMAXn; import static nom.tam.fits.header.Standard.TDMINn; import static nom.tam.fits.header.Standard.TFIELDS; import static nom.tam.fits.header.Standard.TFORMn; import static nom.tam.fits.header.Standard.TLMAXn; import static nom.tam.fits.header.Standard.TLMINn; import static nom.tam.fits.header.Standard.TNULLn; /** * ASCII table data. ASCII tables are meant for human readability without any special tools. However, they are far less * flexible or compact than {@link BinaryTable}. As such, users are generally discouraged from using this type of table * to represent FITS table data. This class only supports scalar entries of type int, long, * float, double, or else String types. * * @see AsciiTableHDU * @see BinaryTable */ @SuppressWarnings("deprecation") public class AsciiTable extends AbstractTableData { private static final int MAX_INTEGER_LENGTH = 10; private static final int FLOAT_MAX_LENGTH = 16; private static final int LONG_MAX_LENGTH = 20; private static final int INT_MAX_LENGTH = 10; private static final int DOUBLE_MAX_LENGTH = 24; /** Whether I10 columns should be treated as int provided that defined limits allow for it. */ private static boolean isI10PreferInt = true; // private static final Logger LOG = Logger.getLogger(AsciiTable.class.getName()); /** The number of rows in the table */ private int nRows; /** The number of fields in the table */ private int nFields; /** The number of bytes in a row */ private int rowLen; /** The null string for the field */ private String[] nulls; /** The type of data in the field */ private Class[] types; /** The offset from the beginning of the row at which the field starts */ private int[] offsets; /** The number of bytes in the field */ private int[] lengths; /** The byte buffer used to read/write the ASCII table */ private byte[] buffer; /** Markers indicating fields that are null */ private boolean[] isNull; /** Column names */ private String[] names; /** * An array of arrays giving the data in the table in binary numbers */ private Object[] data; /** * The parser used to convert from buffer to data. */ private ByteParser bp; /** The actual stream used to input data */ private ArrayDataInput currInput; /** Create an empty ASCII table */ public AsciiTable() { data = new Object[0]; buffer = null; nFields = 0; nRows = 0; rowLen = 0; types = new Class[0]; lengths = new int[0]; offsets = new int[0]; nulls = new String[0]; names = new String[0]; } /** * Creates an ASCII table given a header. For tables that contain integer-valued columns of format I10, * the {@link #setI10PreferInt(boolean)} mayb be used to control whether to treat them as int or as * long values (the latter is the default). * * @param hdr The header describing the table * * @throws FitsException if the operation failed * * @deprecated (for internal use) Visibility may be reduced to the package level in the future. */ public AsciiTable(Header hdr) throws FitsException { this(hdr, isI10PreferInt); } /** *

* Create an ASCII table given a header, with custom integer handling support. *

*

* The preferInt parameter controls how columns with format "I10" are handled; this is * tricky because some, but not all, integers that can be represented in 10 characters can be represented as 32-bit * integers. Setting it true may make it more likely to avoid unexpected type changes during * round-tripping, but it also means that some (large number) data in I10 columns may be impossible to read. *

* * @param hdr The header describing the table * @param preferInt if true, format "I10" columns will be assumed int.class, * provided TLMINn/TLMAXn or TDMINn/TDMAXn limits (if defined) allow it. if * false, I10 columns that have no clear indication of data range will be * assumed long.class. * * @throws FitsException if the operation failed * * @deprecated Use {@link #setI10PreferInt(boolean)} instead prior to reading ASCII tables. */ public AsciiTable(Header hdr, boolean preferInt) throws FitsException { String ext = hdr.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE); if (!ext.equalsIgnoreCase(Standard.XTENSION_ASCIITABLE)) { throw new FitsException("Not an ASCII table header (XTENSION = " + hdr.getStringValue(Standard.XTENSION) + ")"); } nRows = hdr.getIntValue(NAXIS2); nFields = hdr.getIntValue(TFIELDS); rowLen = hdr.getIntValue(NAXIS1); types = new Class[nFields]; offsets = new int[nFields]; lengths = new int[nFields]; nulls = new String[nFields]; names = new String[nFields]; for (int i = 0; i < nFields; i++) { names[i] = hdr.getStringValue(Standard.TTYPEn.n(i + 1), TableHDU.getDefaultColumnName(i)); offsets[i] = hdr.getIntValue(TBCOLn.n(i + 1)) - 1; String s = hdr.getStringValue(TFORMn.n(i + 1)); if (offsets[i] < 0 || s == null) { throw new FitsException("Invalid Specification for column:" + (i + 1)); } s = s.trim(); char c = s.charAt(0); s = s.substring(1); if (s.indexOf('.') > 0) { s = s.substring(0, s.indexOf('.')); } lengths[i] = Integer.parseInt(s); switch (c) { case 'A': types[i] = String.class; break; case 'I': if (lengths[i] == MAX_INTEGER_LENGTH) { types[i] = guessI10Type(i, hdr, preferInt); } else { types[i] = lengths[i] > MAX_INTEGER_LENGTH ? long.class : int.class; } break; case 'F': case 'E': types[i] = float.class; break; case 'D': types[i] = double.class; break; default: throw new FitsException("could not parse column type of ascii table"); } nulls[i] = hdr.getStringValue(TNULLn.n(i + 1)); if (nulls[i] != null) { nulls[i] = nulls[i].trim(); } } } /** * Creates an ASCII table from existing data in column-major format order. * * @param columns The data for scalar-valued columns. Each column must be an array of int[], * long[], float[], double[], or else * String[], containing the same number of elements in each column (the * number of rows). * * @return a new ASCII table with the data. The tables data may be partially independent from the * argument. Modifications to the table data, or that to the argument have undefined * effect on the other object. If it is important to decouple them, you can use a * {@link ArrayFuncs#deepClone(Object)} of your original data as an argument. * * @throws FitsException if the argument is not a suitable representation of FITS data in columns * * @see BinaryTable#fromColumnMajor(Object[]) * * @since 1.19 */ public static AsciiTable fromColumnMajor(Object[] columns) throws FitsException { AsciiTable t = new AsciiTable(); for (int i = 0; i < columns.length; i++) { try { t.addColumn(columns[i]); } catch (Exception e) { throw new FitsException("col[" + i + "]: " + e.getMessage(), e); } } return t; } void setColumnName(int col, String value) throws IllegalArgumentException, IndexOutOfBoundsException, HeaderCardException { HeaderCard.validateChars(value); names[col] = value; } /** * Checks if the integer value of a specific key requires long value type to store. * * @param h the header * @param key the keyword to check * * @return true if the keyword exists and has an integer value that is outside the range of * int. Otherwise false * * @see #guessI10Type(int, Header, boolean) */ private boolean requiresLong(Header h, IFitsHeader key, Long dft) { long l = h.getLongValue(key, dft); if (l == dft) { return false; } return (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE); } /** * Guesses what type of values to use to return I10 type table values. Depending on the range of represented values * I10 may fit into int types, or else require long type arrays. Therefore, the method * checks for the presence of standard column limit keywords TLMINn/TLMAXn and TDMINn/TDMAXn and if these exist and * are outside of the range of an int then the call will return long.class. If the header * does not define the data limits (fully), it will return the class the caller prefers. Otherwise (data limits were * defined and fit into the int range) int.class will be returned. * * @param col the 0-based table column index * @param h the header * @param preferInt whether we prefer int.class over long.class in case the header does * not provide us with a clue. * * @return long.class if the data requires long or we prefer it. Othwerwise * int.class * * @see #AsciiTable(Header, boolean) */ private Class guessI10Type(int col, Header h, boolean preferInt) { col++; if (requiresLong(h, TLMINn.n(col), Long.MAX_VALUE) || requiresLong(h, TLMAXn.n(col), Long.MIN_VALUE) || requiresLong(h, TDMINn.n(col), Long.MAX_VALUE) || requiresLong(h, TDMAXn.n(col), Long.MIN_VALUE)) { return long.class; } if ((h.containsKey(TLMINn.n(col)) || h.containsKey(TDMINn.n(col))) // && (h.containsKey(TLMAXn.n(col)) || h.containsKey(TDMAXn.n(col)))) { // There are keywords defining both min/max values, and none of them require long types... return int.class; } return preferInt ? int.class : long.class; } /** * Return the data type in the specified column, such as int.class or String.class. * * @param col The 0-based column index * * @return the class of data in the specified column. * * @since 1.16 */ public final Class getColumnType(int col) { return types[col]; } int addColInfo(int col, Cursor iter) { String tform = null; if (types[col] == String.class) { tform = "A" + lengths[col]; } else if (types[col] == int.class || types[col] == long.class) { tform = "I" + lengths[col]; } else if (types[col] == float.class) { tform = "E" + lengths[col] + ".0"; } else if (types[col] == double.class) { tform = "D" + lengths[col] + ".0"; } Standard.context(AsciiTable.class); if (names[col] != null) { iter.add(HeaderCard.create(Standard.TTYPEn.n(col + 1), names[col])); } iter.add(HeaderCard.create(Standard.TFORMn.n(col + 1), tform)); iter.add(HeaderCard.create(Standard.TBCOLn.n(col + 1), offsets[col] + 1)); Standard.context(null); return lengths[col]; } @Override public int addColumn(Object newCol) throws FitsException, IllegalArgumentException { if (newCol == null) { throw new FitsException("data is null"); } if (!newCol.getClass().isArray()) { throw new IllegalArgumentException("Not an array: " + newCol.getClass().getName()); } int maxLen = 1; if (newCol instanceof String[]) { String[] sa = (String[]) newCol; for (String element : sa) { if (element != null && element.length() > maxLen) { maxLen = element.length(); } } } else if (newCol instanceof double[]) { maxLen = DOUBLE_MAX_LENGTH; } else if (newCol instanceof int[]) { maxLen = INT_MAX_LENGTH; } else if (newCol instanceof long[]) { maxLen = LONG_MAX_LENGTH; } else if (newCol instanceof float[]) { maxLen = FLOAT_MAX_LENGTH; } else { throw new FitsException( "No AsciiTable support for elements of " + newCol.getClass().getComponentType().getName()); } addColumn(newCol, maxLen); // Invalidate the buffer buffer = null; return nFields; } /** * Adds an ASCII table column with the specified ASCII text width for storing its elements. * * @param newCol The new column data, which must be one of: int[], * long[], float[], double[], or else * String[]. If the table already contains data, the length of the * array must match the number of rows already contained in the table. * @param width the ASCII text width of the for the column entries (without the string * termination). * * @return the number of columns after this one is added. * * @throws IllegalArgumentException if the column data is not an array or the specified text width is * ≤1. * @throws FitsException if the column us of an unsupported data type or if the number of entries does * not match the number of rows already contained in the table. * * @see #addColumn(Object) */ public int addColumn(Object newCol, int width) throws FitsException, IllegalArgumentException { if (width < 1) { throw new IllegalArgumentException("Illegal ASCII column width: " + width); } if (!newCol.getClass().isArray()) { throw new IllegalArgumentException("Not an array: " + newCol.getClass().getName()); } if (nFields > 0 && Array.getLength(newCol) != nRows) { throw new FitsException( "Mismatched number of rows: expected " + nRows + ", got " + Array.getLength(newCol) + "rows."); } if (nFields == 0) { nRows = Array.getLength(newCol); } Class type = ArrayFuncs.getBaseClass(newCol); if (type != int.class && type != long.class && type != float.class && type != double.class && type != String.class) { throw new FitsException("No AsciiTable support for elements of " + type.getName()); } data = Arrays.copyOf(data, nFields + 1); offsets = Arrays.copyOf(offsets, nFields + 1); lengths = Arrays.copyOf(lengths, nFields + 1); types = Arrays.copyOf(types, nFields + 1); nulls = Arrays.copyOf(nulls, nFields + 1); names = Arrays.copyOf(names, nFields + 1); data[nFields] = newCol; offsets[nFields] = rowLen + 1; lengths[nFields] = width; types[nFields] = ArrayFuncs.getBaseClass(newCol); names[nFields] = TableHDU.getDefaultColumnName(nFields); rowLen += width + 1; if (isNull != null) { boolean[] newIsNull = new boolean[nRows * (nFields + 1)]; // Fix the null pointers. int add = 0; for (int i = 0; i < isNull.length; i++) { if (i % nFields == 0) { add++; } if (isNull[i]) { newIsNull[i + add] = true; } } isNull = newIsNull; } nFields++; // Invalidate the buffer buffer = null; return nFields; } /** * Beware that adding rows to ASCII tables may be very inefficient. Avoid addding more than a few rows if you can. */ @Override public int addRow(Object[] newRow) throws FitsException { try { // If there are no fields, then this is the // first row. We need to add in each of the columns // to get the descriptors set up. if (nFields == 0) { for (Object element : newRow) { addColumn(element); } } else { for (int i = 0; i < nFields; i++) { Object o = ArrayFuncs.newInstance(types[i], nRows + 1); System.arraycopy(data[i], 0, o, 0, nRows); System.arraycopy(newRow[i], 0, o, nRows, 1); data[i] = o; } nRows++; } // Invalidate the buffer buffer = null; return nRows; } catch (Exception e) { throw new FitsException("Error adding row:" + e.getMessage(), e); } } @Override public void deleteColumns(int start, int len) throws FitsException { ensureData(); Object[] newData = new Object[nFields - len]; int[] newOffsets = new int[nFields - len]; int[] newLengths = new int[nFields - len]; Class[] newTypes = new Class[nFields - len]; String[] newNulls = new String[nFields - len]; // Copy in the initial stuff... System.arraycopy(data, 0, newData, 0, start); // Don't do the offsets here. System.arraycopy(lengths, 0, newLengths, 0, start); System.arraycopy(types, 0, newTypes, 0, start); System.arraycopy(nulls, 0, newNulls, 0, start); // Copy in the final System.arraycopy(data, start + len, newData, start, nFields - start - len); // Don't do the offsets here. System.arraycopy(lengths, start + len, newLengths, start, nFields - start - len); System.arraycopy(types, start + len, newTypes, start, nFields - start - len); System.arraycopy(nulls, start + len, newNulls, start, nFields - start - len); for (int i = start; i < start + len; i++) { rowLen -= lengths[i] + 1; } data = newData; offsets = newOffsets; lengths = newLengths; types = newTypes; nulls = newNulls; if (isNull != null) { boolean found = false; boolean[] newIsNull = new boolean[nRows * (nFields - len)]; for (int i = 0; i < nRows; i++) { int oldOff = nFields * i; int newOff = (nFields - len) * i; for (int col = 0; col < start; col++) { newIsNull[newOff + col] = isNull[oldOff + col]; found = found || isNull[oldOff + col]; } for (int col = start + len; col < nFields; col++) { newIsNull[newOff + col - len] = isNull[oldOff + col]; found = found || isNull[oldOff + col]; } } if (found) { isNull = newIsNull; } else { isNull = null; } } // Invalidate the buffer buffer = null; nFields -= len; } /** * Beware that repeatedly deleting rows from ASCII tables may be very inefficient. Avoid calling this more than once * (or a few times) if you can. */ @Override public void deleteRows(int start, int len) throws FitsException { if (nRows == 0 || start < 0 || start >= nRows || len <= 0) { return; } if (start + len > nRows) { len = nRows - start; } ensureData(); for (int i = 0; i < nFields; i++) { try { Object o = ArrayFuncs.newInstance(types[i], nRows - len); System.arraycopy(data[i], 0, o, 0, start); System.arraycopy(data[i], start + len, o, start, nRows - len - start); data[i] = o; } catch (Exception e) { throw new FitsException("Error deleting row: " + e.getMessage(), e); } } nRows -= len; } @Override protected void loadData(ArrayDataInput in) throws IOException, FitsException { currInput = in; if (buffer == null) { getBuffer((long) nRows * rowLen, 0); } data = new Object[nFields]; for (int i = 0; i < nFields; i++) { data[i] = ArrayFuncs.newInstance(types[i], nRows); } bp.setOffset(0); int rowOffset; for (int i = 0; i < nRows; i++) { rowOffset = rowLen * i; for (int j = 0; j < nFields; j++) { try { if (!extractElement(rowOffset + offsets[j], lengths[j], data, j, i, nulls[j])) { if (isNull == null) { isNull = new boolean[nRows * nFields]; } isNull[j + i * nFields] = true; } } catch (ArrayIndexOutOfBoundsException e) { throw new FitsException("not enough data: " + e, e); } } } } @Override public void read(ArrayDataInput in) throws FitsException { currInput = in; super.read(in); } /** * Move an element from the buffer into a data array. * * @param offset The offset within buffer at which the element starts. * @param length The number of bytes in the buffer for the element. * @param array An array of objects, each of which is a simple array. * @param col Which element of array is to be modified? * @param row Which index into that element is to be modified? * @param nullFld What string signifies a null element? * * @throws FitsException if the operation failed */ private boolean extractElement(int offset, int length, Object[] array, int col, int row, String nullFld) throws FitsException { bp.setOffset(offset); if (nullFld != null) { String s = bp.getString(length); if (s.trim().equals(nullFld)) { return false; } bp.skip(-length); } try { if (array[col] instanceof String[]) { ((String[]) array[col])[row] = bp.getString(length); } else if (array[col] instanceof int[]) { ((int[]) array[col])[row] = bp.getInt(length); } else if (array[col] instanceof float[]) { ((float[]) array[col])[row] = bp.getFloat(length); } else if (array[col] instanceof double[]) { ((double[]) array[col])[row] = bp.getDouble(length); } else if (array[col] instanceof long[]) { ((long[]) array[col])[row] = bp.getLong(length); } else { throw new FitsException("Invalid type for ASCII table conversion:" + array[col]); } } catch (FormatException e) { throw new FitsException("Error parsing data at row,col:" + row + "," + col + " ", e); } return true; } @Override protected void fillHeader(Header h) { h.deleteKey(Standard.SIMPLE); h.deleteKey(Standard.EXTEND); Standard.context(AsciiTable.class); Cursor c = h.iterator(); c.add(HeaderCard.create(Standard.XTENSION, Standard.XTENSION_ASCIITABLE)); c.add(HeaderCard.create(Standard.BITPIX, Bitpix.BYTE.getHeaderValue())); c.add(HeaderCard.create(Standard.NAXIS, 2)); c.add(HeaderCard.create(Standard.NAXIS1, rowLen)); c.add(HeaderCard.create(Standard.NAXIS2, nRows)); c.add(HeaderCard.create(Standard.PCOUNT, 0)); c.add(HeaderCard.create(Standard.GCOUNT, 1)); c.add(HeaderCard.create(Standard.TFIELDS, nFields)); for (int i = 0; i < nFields; i++) { addColInfo(i, c); } Standard.context(null); } /** * Read some data into the buffer. */ private void getBuffer(long size, long offset) throws IOException, FitsException { if (currInput == null) { throw new IOException("No stream open to read"); } if (size > Integer.MAX_VALUE) { throw new FitsException("Cannot read ASCII table > 2 GB"); } buffer = new byte[(int) size]; if (offset != 0) { FitsUtil.reposition(currInput, offset); } currInput.readFully(buffer); bp = new ByteParser(buffer); } /** *

* Returns the data for a particular column in as a flattened 1D array of elements. See {@link #addColumn(Object)} * for more information about the format of data elements in general. *

* * @param col The 0-based column index. * * @return an array of primitives (for scalar columns), or else an Object[] array. * * @throws FitsException if the table could not be accessed * * @see #setColumn(int, Object) * @see #getElement(int, int) * @see #getNCols() */ @Override public Object getColumn(int col) throws FitsException { ensureData(); return data[col]; } @Override protected Object[] getCurrentData() { return data; } @Override public Object[] getData() throws FitsException { return (Object[]) super.getData(); } @Override public Object getElement(int row, int col) throws FitsException { if (data != null) { return singleElement(row, col); } return parseSingleElement(row, col); } @Override public int getNCols() { return nFields; } @Override public int getNRows() { return nRows; } @Override public Object[] getRow(int row) throws FitsException { if (data != null) { return singleRow(row); } return parseSingleRow(row); } /** * Get the number of bytes in a row * * @return The number of bytes for a single row in the table. */ public int getRowLen() { return rowLen; } @Override protected long getTrueSize() { return (long) nRows * rowLen; } /** * Checks if an element is null. * * @param row The 0-based row * @param col The 0-based column * * @return if the given element has been nulled. */ public boolean isNull(int row, int col) { if (isNull != null) { return isNull[row * nFields + col]; } return false; } /** * Read a single element from the table. This returns an array of dimension 1. * * @throws FitsException if the operation failed */ private Object parseSingleElement(int row, int col) throws FitsException { Object[] res = new Object[1]; try { getBuffer(lengths[col], getFileOffset() + (long) row * (long) rowLen + offsets[col]); } catch (IOException e) { buffer = null; throw new FitsException("Unable to read element", e); } res[0] = ArrayFuncs.newInstance(types[col], 1); boolean success = extractElement(0, lengths[col], res, 0, 0, nulls[col]); buffer = null; return success ? res[0] : null; } /** * Read a single row from the table. This returns a set of arrays of dimension 1. * * @throws FitsException if the operation failed */ private Object[] parseSingleRow(int row) throws FitsException { Object[] res = new Object[nFields]; try { getBuffer(rowLen, getFileOffset() + (long) row * (long) rowLen); } catch (IOException e) { throw new FitsException("Unable to read row", e); } for (int i = 0; i < nFields; i++) { res[i] = ArrayFuncs.newInstance(types[i], 1); if (!extractElement(offsets[i], lengths[i], res, i, 0, nulls[i])) { res[i] = null; } } // Invalidate buffer for future use. buffer = null; return res; } @Override public void setColumn(int col, Object newData) throws FitsException { ensureData(); if (col < 0 || col >= nFields || newData.getClass() != data[col].getClass() || Array.getLength(newData) != Array.getLength(data[col])) { throw new FitsException("Invalid column/column mismatch:" + col); } data[col] = newData; // Invalidate the buffer. buffer = null; } @Override public void setElement(int row, int col, Object newData) throws FitsException { ensureData(); try { System.arraycopy(newData, 0, data[col], row, 1); } catch (Exception e) { throw new FitsException("Incompatible element:" + row + "," + col, e); } setNull(row, col, false); // Invalidate the buffer buffer = null; } /** * Mark (or unmark) an element as null. Note that if this FITS file is latter written out, a TNULL keyword needs to * be defined in the corresponding header. This routine does not add an element for String columns. * * @param row The 0-based row. * @param col The 0-based column. * @param flag True if the element is to be set to null. */ public void setNull(int row, int col, boolean flag) { if (flag) { if (isNull == null) { isNull = new boolean[nRows * nFields]; } isNull[col + row * nFields] = true; } else if (isNull != null) { isNull[col + row * nFields] = false; } // Invalidate the buffer buffer = null; } /** * Set the null string for a columns. This is not a public method since we want users to call the method in * AsciiTableHDU and update the header also. */ void setNullString(int col, String newNull) { if (col >= 0 && col < nulls.length) { nulls[col] = newNull; } } @Override public void setRow(int row, Object[] newData) throws FitsException { if (row < 0 || row > nRows) { throw new FitsException("Invalid row in setRow"); } ensureData(); for (int i = 0; i < nFields; i++) { try { System.arraycopy(newData[i], 0, data[i], row, 1); } catch (Exception e) { throw new FitsException("Unable to modify row: incompatible data:" + row, e); } setNull(row, i, false); } // Invalidate the buffer buffer = null; } /** * Extract a single element from a table. This returns an array of length 1. */ private Object singleElement(int row, int col) { Object res = null; if (isNull == null || !isNull[row * nFields + col]) { res = ArrayFuncs.newInstance(types[col], 1); System.arraycopy(data[col], row, res, 0, 1); } return res; } /** * Extract a single row from a table. This returns an array of Objects each of which is an array of length 1. */ private Object[] singleRow(int row) { Object[] res = new Object[nFields]; for (int i = 0; i < nFields; i++) { if (isNull == null || !isNull[row * nFields + i]) { res[i] = ArrayFuncs.newInstance(types[i], 1); System.arraycopy(data[i], row, res[i], 0, 1); } } return res; } /** * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to (re)wrap tables in a * new HDU and editing the header as necessary to incorporate custom entries. May be removed from * the API in the future. */ @Override public void updateAfterDelete(int oldNCol, Header hdr) throws FitsException { int offset = 0; for (int i = 0; i < nFields; i++) { offsets[i] = offset; hdr.addValue(TBCOLn.n(i + 1), offset + 1); offset += lengths[i] + 1; } for (int i = nFields; i < oldNCol; i++) { hdr.deleteKey(TBCOLn.n(i + 1)); } hdr.addValue(NAXIS1, rowLen); } @Override public void write(ArrayDataOutput str) throws FitsException { // Make sure we have the data in hand. if (str != currInput) { ensureData(); } // If buffer is still around we can just reuse it, // since nothing we've done has invalidated it. if (data == null) { throw new FitsException("Attempt to write undefined ASCII Table"); } if ((long) nRows * rowLen > Integer.MAX_VALUE) { throw new FitsException("Cannot write ASCII table > 2 GB"); } buffer = new byte[nRows * rowLen]; bp = new ByteParser(buffer); for (int i = 0; i < buffer.length; i++) { buffer[i] = (byte) ' '; } ByteFormatter bf = new ByteFormatter(); for (int i = 0; i < nRows; i++) { for (int j = 0; j < nFields; j++) { int offset = i * rowLen + offsets[j]; int len = lengths[j]; if (isNull != null && isNull[i * nFields + j]) { if (nulls[j] == null) { throw new FitsException("No null value set when needed"); } bf.format(nulls[j], buffer, offset, len); } else if (types[j] == String.class) { String[] s = (String[]) data[j]; bf.format(s[i], buffer, offset, len); } else if (types[j] == int.class) { int[] ia = (int[]) data[j]; bf.format(ia[i], buffer, offset, len); } else if (types[j] == float.class) { float[] fa = (float[]) data[j]; bf.format(fa[i], buffer, offset, len); } else if (types[j] == double.class) { double[] da = (double[]) data[j]; bf.format(da[i], buffer, offset, len); } else if (types[j] == long.class) { long[] la = (long[]) data[j]; bf.format(la[i], buffer, offset, len); } } } // Now write the buffer. try { str.write(buffer); FitsUtil.pad(str, buffer.length, (byte) ' '); } catch (IOException e) { throw new FitsException("Error writing ASCII Table data", e); } } @Override public AsciiTableHDU toHDU() { Header h = new Header(); fillHeader(h); return new AsciiTableHDU(h, this); } /** *

* Controls how columns with format "I10" are handled; this is tricky because some, but not all, * integers that can be represented in 10 characters form 32-bit integers. Setting it true may make it * more likely to avoid unexpected type changes during round-tripping, but it also means that some values in I10 * columns may be impossible to read. The default behavior is to assume true, and thus to treat I10 * columns as int values. *

* * @param value if true, format "I10" columns will be assumed int.class, provided * TLMINn/TLMAXn or TDMINn/TDMAXn limits (if defined) allow it. if false, I10 columns * that have no clear indication of data range will be assumed long.class. * * @since 1.19 * * @see AsciiTable#isI10PreferInt() */ public static void setI10PreferInt(boolean value) { isI10PreferInt = value; } /** * Checks if I10 columns should be treated as containing 32-bit int values, rather than 64-bit * long values, when possible. * * @return true if I10 columns should be treated as containing 32-bit int values, * otherwise false. * * @since 1.19 * * @see #setI10PreferInt(boolean) */ public static boolean isI10PreferInt() { return isI10PreferInt; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/AsciiTableHDU.java000066400000000000000000000243141476377620500243120ustar00rootroot00000000000000package nom.tam.fits; import java.io.PrintStream; import nom.tam.fits.header.IFitsHeader; import nom.tam.fits.header.Standard; import nom.tam.util.ArrayFuncs; import nom.tam.util.Cursor; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.fits.header.Standard.NAXIS1; import static nom.tam.fits.header.Standard.NAXIS2; import static nom.tam.fits.header.Standard.TBCOLn; import static nom.tam.fits.header.Standard.TFIELDS; import static nom.tam.fits.header.Standard.TFORMn; import static nom.tam.fits.header.Standard.TNULLn; import static nom.tam.fits.header.Standard.TTYPEn; import static nom.tam.fits.header.Standard.TUNITn; import static nom.tam.fits.header.Standard.TZEROn; import static nom.tam.fits.header.Standard.XTENSION; import static nom.tam.fits.header.Standard.XTENSION_ASCIITABLE; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * ASCII table header/data unit. ASCII table HDUs were desgined for human readability, e.g. on a console, without any * special tools. However, they are far less flexible or compact than {@link BinaryTableHDU}. As such, users are * generally discouraged from using this type of HDU to encapsulate FITS table data. * {@link FitsFactory#setUseAsciiTables(boolean)} can be toggled to adjust whether {@link Fits#makeHDU(Object)} or * similar methods should construct ASCII tables when possible. (The default setting is to produce binary tables * always.) * * @see AsciiTable * @see BinaryTableHDU */ public class AsciiTableHDU extends TableHDU { /** * The standard column stems for an ASCII table. Note that TBCOL is not included here -- it needs to be handled * specially since it does not simply shift. */ private static final IFitsHeader[] KEY_STEMS = {TFORMn, TZEROn, TNULLn, TTYPEn, TUNITn}; /** * Create an ASCII table header/data unit. * * @deprecated (for internal use) Its visibility should be reduced to package level in the future. * * @param h the template specifying the ASCII table. * @param d the FITS data structure containing the table data. */ public AsciiTableHDU(Header h, AsciiTable d) { super(h, d); } @Override protected final String getCanonicalXtension() { return XTENSION_ASCIITABLE; } /** * @deprecated (for internal use) Use {@link AsciiTable#fromColumnMajor(Object[])} instead. * Will reduce visibility in the future * * @return a ASCII table data structure from an array of objects representing the columns. * * @param o the array of object to create the ASCII table * * @throws FitsException if the table could not be created. */ @Deprecated public static AsciiTable encapsulate(Object o) throws FitsException { return AsciiTable.fromColumnMajor((Object[]) o); } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return true if this data is usable as an ASCII table. * * @param o object representing the data */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isData(Object o) { if (o instanceof Object[]) { for (Object element : (Object[]) o) { if (!(element instanceof String[]) && // !(element instanceof int[]) && // !(element instanceof long[]) && // !(element instanceof float[]) && // !(element instanceof double[])) { return false; } } return true; } return false; } /** * Check that this is a valid ascii table header. * * @deprecated (for internal use) Will reduce visibility in the future * * @param header to validate. * * @return true if this is an ascii table header. */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isHeader(Header header) { String xtension = header.getStringValue(XTENSION); xtension = xtension == null ? "" : xtension.trim(); return XTENSION_ASCIITABLE.equals(xtension); } /** * Prepares a data object into which the actual data can be read from an input subsequently or at a later time. * * @deprecated (for internal use) Will reduce visibility in the future * * @param hdr The FITS header that describes the data * * @return A data object that support reading content from a stream. * * @throws FitsException if the data could not be prepared to prescriotion. */ @Deprecated public static AsciiTable manufactureData(Header hdr) throws FitsException { return new AsciiTable(hdr); } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return a created header to match the input data. * * @param d data to create a header for * * @throws FitsException if the header could not b e created */ @Deprecated public static Header manufactureHeader(Data d) throws FitsException { Header hdr = new Header(); d.fillHeader(hdr); hdr.iterator(); return hdr; } @Override public void setColumnName(int index, String name, String comment) throws IndexOutOfBoundsException, HeaderCardException { super.setColumnName(index, name, comment); myData.setColumnName(index, name); } @SuppressWarnings("deprecation") @Override public int addColumn(Object newCol) throws FitsException { Standard.context(AsciiTable.class); myData.addColumn(newCol); // Move the iterator to point after all the data describing // the previous column. Cursor iter = myHeader.positionAfterIndex(TBCOLn, myData.getNCols()); int rowlen = myData.addColInfo(getNCols() - 1, iter); int oldRowlen = myHeader.getIntValue(NAXIS1); myHeader.setNaxis(1, rowlen + oldRowlen); super.addColumn(newCol); Standard.context(null); return getNCols(); } @Override protected IFitsHeader[] columnKeyStems() { return KEY_STEMS; } @Override public void info(PrintStream stream) { stream.println("ASCII Table:"); stream.println(" Header:"); stream.println(" Number of fields:" + myHeader.getIntValue(TFIELDS)); stream.println(" Number of rows: " + myHeader.getIntValue(NAXIS2)); stream.println(" Length of row: " + myHeader.getIntValue(NAXIS1)); stream.println(" Data:"); Object[] data = (Object[]) getKernel(); for (int i = 0; i < getNCols(); i++) { stream.println(" " + i + ":" + ArrayFuncs.arrayDescription(data[i])); } } /** * Checks if a table entry is null * * @param row row index of the element * @param col column index of the element * * @return true if the specified element is null * * @see #setNull(int, int, boolean) * @see AsciiTable#isNull(int, int) */ public boolean isNull(int row, int col) { return myData.isNull(row, col); } /** * Mark an entry as null. * * @param row row index of the element * @param col column index of the element * @param flag set to null or not * * @see #isNull(int, int) * @see AsciiTable#setNull(int, int, boolean) */ public void setNull(int row, int col, boolean flag) { if (flag) { String nullStr = myHeader.getStringValue(TNULLn.n(col + 1)); if (nullStr == null) { setNullString(col, "NULL"); } } myData.setNull(row, col, flag); } /** * Set the null string for a column. * * @param col the column index * @param newNull the String representing null * * @throws IllegalArgumentException if the string argument contains characters that are not allowed in FITS headers. * That is if it contains characters outside the range of 0x20 thru 0x7E. */ public void setNullString(int col, String newNull) throws IllegalArgumentException { myHeader.positionAfterIndex(TBCOLn, col + 1); HeaderCard card = HeaderCard.create(TNULLn.n(col + 1), newNull); myHeader.deleteKey(card.getKey()); myHeader.addLine(card); myData.setNullString(col, newNull); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/BasicHDU.java000066400000000000000000001105611476377620500233330ustar00rootroot00000000000000package nom.tam.fits; import java.io.IOException; import java.io.PrintStream; import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.fits.header.Bitpix; import nom.tam.fits.header.Checksum; import nom.tam.fits.header.IFitsHeader; import nom.tam.fits.header.Standard; import nom.tam.fits.utilities.FitsCheckSum; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.FitsOutput; import nom.tam.util.RandomAccess; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.fits.header.Standard.AUTHOR; import static nom.tam.fits.header.Standard.BLANK; import static nom.tam.fits.header.Standard.BSCALE; import static nom.tam.fits.header.Standard.BUNIT; import static nom.tam.fits.header.Standard.BZERO; import static nom.tam.fits.header.Standard.DATAMAX; import static nom.tam.fits.header.Standard.DATAMIN; import static nom.tam.fits.header.Standard.DATE; import static nom.tam.fits.header.Standard.DATE_OBS; import static nom.tam.fits.header.Standard.EPOCH; import static nom.tam.fits.header.Standard.EQUINOX; import static nom.tam.fits.header.Standard.GCOUNT; import static nom.tam.fits.header.Standard.INSTRUME; import static nom.tam.fits.header.Standard.NAXIS; import static nom.tam.fits.header.Standard.NAXISn; import static nom.tam.fits.header.Standard.OBJECT; import static nom.tam.fits.header.Standard.OBSERVER; import static nom.tam.fits.header.Standard.ORIGIN; import static nom.tam.fits.header.Standard.PCOUNT; import static nom.tam.fits.header.Standard.REFERENC; import static nom.tam.fits.header.Standard.TELESCOP; import static nom.tam.util.LoggerHelper.getLogger; /** * Abstract base class for all header-data unit (HDU) types. A HDU is a self-contained building block of the FITS files, * which encapsulates information on a particular data object such as an image or table. As the name implies, HDUs * constitute of a header and data entities, which can be accessed separately (via the {@link #getHeader()} and * {@link #getData()} methods respectively). The {@link Header} class provides many functions to add, delete and read * header keywords in HDUs in a variety of formats. The {@link Data} class, and its concrete subclassses provide access * to the specific data object that the HDU encapsulates. It provides basic functionality for an HDU. * * @param the generic type of data contained in this HDU instance. */ public abstract class BasicHDU implements FitsElement { private static final int MAX_NAXIS_ALLOWED = 999; private static final Logger LOG = getLogger(BasicHDU.class); /** * @deprecated Use {@link Bitpix#VALUE_FOR_BYTE} instead. */ @Deprecated public static final int BITPIX_BYTE = 8; /** * @deprecated Use {@link Bitpix#VALUE_FOR_SHORT} instead. */ @Deprecated public static final int BITPIX_SHORT = 16; /** * @deprecated Use {@link Bitpix#VALUE_FOR_INT} instead. */ @Deprecated public static final int BITPIX_INT = 32; /** * @deprecated Use {@link Bitpix#VALUE_FOR_LONG} instead. */ @Deprecated public static final int BITPIX_LONG = 64; /** * @deprecated Use {@link Bitpix#VALUE_FOR_FLOAT} instead. */ @Deprecated public static final int BITPIX_FLOAT = -32; /** * @deprecated Use {@link Bitpix#VALUE_FOR_DOUBLE} instead. */ @Deprecated public static final int BITPIX_DOUBLE = -64; /** The associated header. */ protected Header myHeader = null; /** The associated data unit. */ protected DataClass myData = null; /** * Creates a new HDU from the specified FITS header and associated data object. * * @deprecated intended for internal use. Its visibility should be reduced to package level in the future. * * @param myHeader the FITS header describing the data and any user-specific keywords * @param myData the corresponding data object */ protected BasicHDU(Header myHeader, DataClass myData) { setHeader(myHeader); this.myData = myData; } private void setHeader(Header header) { this.myHeader = header; if (header != null) { this.myHeader.assignTo(this); } } /** * @deprecated Use {@link NullDataHDU} instead. Gets a HDU with no data, only header. * * @return an HDU without content */ @Deprecated public static NullDataHDU getDummyHDU() { return new NullDataHDU(); } /** * Checks that this is a valid header for the HDU. This method is static but should be implemented by all * subclasses. * * @deprecated (for internal use) Will be removed as it serves no purpose. * * @param header to validate. * * @return true if this is a valid header. */ public static boolean isHeader(Header header) { return false; } /** * @deprecated (for internal use) Will be removed as it serves no purpose. * * @return if this object can be described as a FITS image. This method is static but should be implemented by * all subclasses. * * @param o The Object being tested. */ public static boolean isData(Object o) { return false; } /** * Add information to the header. * * @param key key to add to the header * @param val value for the key to add * * @throws HeaderCardException if the card does not follow the specification * * @see #addValue(String, boolean, String) * @see #addValue(IFitsHeader, int) * @see #addValue(IFitsHeader, double) * @see #addValue(IFitsHeader, String) */ public void addValue(IFitsHeader key, boolean val) throws HeaderCardException { myHeader.addValue(key.key(), val, key.comment()); } /** * Add information to the header. * * @param key key to add to the header * @param val value for the key to add * * @throws HeaderCardException if the card does not follow the specification * * @see #addValue(String, boolean, String) * @see #addValue(IFitsHeader, boolean) * @see #addValue(IFitsHeader, int) * @see #addValue(IFitsHeader, String) */ public void addValue(IFitsHeader key, double val) throws HeaderCardException { myHeader.addValue(key.key(), val, key.comment()); } /** * Add information to the header. * * @param key key to add to the header * @param val value for the key to add * * @throws HeaderCardException if the card does not follow the specification * * @see #addValue(String, boolean, String) * @see #addValue(IFitsHeader, boolean) * @see #addValue(IFitsHeader, double) * @see #addValue(IFitsHeader, String) */ public void addValue(IFitsHeader key, int val) throws HeaderCardException { myHeader.addValue(key.key(), val, key.comment()); } /** * Add information to the header. * * @param key key to add to the header * @param val value for the key to add * * @throws HeaderCardException if the card does not follow the specification * * @see #addValue(String, boolean, String) * @see #addValue(IFitsHeader, boolean) * @see #addValue(IFitsHeader, int) * @see #addValue(IFitsHeader, double) */ public void addValue(IFitsHeader key, String val) throws HeaderCardException { myHeader.addValue(key.key(), val, key.comment()); } /** * Add information to the header. * * @param key key to add to the header * @param val value for the key to add * @param comment comment for the key/value pair * * @throws HeaderCardException if the card does not follow the specification * * @see #addValue(IFitsHeader, boolean) * @see #addValue(String, int, String) * @see #addValue(String, double, String) * @see #addValue(String, String, String) */ public void addValue(String key, boolean val, String comment) throws HeaderCardException { myHeader.addValue(key, val, comment); } /** * Add information to the header. * * @param key key to add to the header * @param val value for the key to add * @param comment comment for the key/value pair * * @throws HeaderCardException if the card does not follow the specification * * @see #addValue(IFitsHeader, double) * @see #addValue(String, boolean, String) * @see #addValue(String, int, String) * @see #addValue(String, String, String) */ public void addValue(String key, double val, String comment) throws HeaderCardException { myHeader.addValue(key, val, comment); } /** * Add information to the header. * * @param key key to add to the header * @param val value for the key to add * @param comment comment for the key/value pair * * @throws HeaderCardException if the card does not follow the specification * * @see #addValue(IFitsHeader, int) * @see #addValue(String, boolean, String) * @see #addValue(String, double, String) * @see #addValue(String, String, String) */ public void addValue(String key, int val, String comment) throws HeaderCardException { myHeader.addValue(key, val, comment); } /** * Add information to the header. * * @param key key to add to the header * @param val value for the key to add * @param comment comment for the key/value pair * * @throws HeaderCardException if the card does not follow the specification * * @see #addValue(IFitsHeader, String) * @see #addValue(String, boolean, String) * @see #addValue(String, double, String) * @see #addValue(String, int, String) */ public void addValue(String key, String val, String comment) throws HeaderCardException { myHeader.addValue(key, val, comment); } /** * Checks if this HDU can be used as a primary HDU. For historical reasons FITS only allows certain HDU types to * appear at the head of FITS files. Further HDU types can only be added as extensions after the first HDU. If this * call returns false you may need to add e.g. a dummy {@link NullDataHDU} as the primary HDU at the * beginning of the FITS before you can add this one. * * @return Indicate whether HDU can be primary HDU. This method must be overriden in HDU types which can appear at * the beginning of a FITS file. */ final boolean canBePrimary() { return Standard.XTENSION_IMAGE.equals(getCanonicalXtension()); } /** * Return the name of the person who compiled the information in the data associated with this header. * * @return either null or a String object */ public String getAuthor() { return myHeader.getStringValue(AUTHOR); } /** * In FITS files the index represented by NAXIS1 is the index that changes most rapidly. This reflectsf the behavior * of Fortran where there are true multidimensional arrays. In Java in a multidimensional array is an array of * arrays and the first index is the index that changes slowest. So at some point a client of the library is going * to have to invert the order. E.g., if I have a FITS file will * *
     * BITPIX=16
     * NAXIS1=10
     * NAXIS2=20
     * NAXIS3=30
     * 
* * this will be read into a Java array short[30][20][10] so it makes sense to me at least that the returned * dimensions are 30,20,10 * * @return the dimensions of the axis. * * @throws FitsException if the axis are configured wrong. */ public int[] getAxes() throws FitsException { int nAxis = myHeader.getIntValue(NAXIS, 0); if (nAxis < 0) { throw new FitsException("Negative NAXIS value " + nAxis); } if (nAxis > MAX_NAXIS_ALLOWED) { throw new FitsException("NAXIS value " + nAxis + " too large"); } if (nAxis == 0) { return null; } int[] axes = new int[nAxis]; for (int i = 1; i <= nAxis; i++) { axes[nAxis - i] = myHeader.getIntValue(NAXISn.n(i), 0); } return axes; } /** * Return the Bitpix enum type for this HDU. * * @return The Bitpix enum object for this HDU. * * @throws FitsException if the BITPIX value in the header is absent or invalid. * * @since 1.16 * * @see #getBitPix() */ public Bitpix getBitpix() throws FitsException { return Bitpix.fromHeader(myHeader); } /** * Return the BITPIX integer value as stored in the FIS header. * * @return The BITPIX integer values for this HDU as it appears in the header. * * @throws FitsException if the BITPIX value in the header is absent or invalid. * * @deprecated (for internal use) Will reduce visibility or remove entirely in the future. * * @see #getBitpix() */ public final int getBitPix() throws FitsException { return getBitpix().getHeaderValue(); } /** * Returns the name of the physical unit in which images are represented. * * @deprecated This is only applicable to {@link ImageHDU} or {@link RandomGroupsHDU} and not for other HDU or data * types. * * @return the standard name of the physical unit in which the image is expressed, e.g. * "Jy beam^{-1}". */ public String getBUnit() { return myHeader.getStringValue(BUNIT); } /** * Returns the integer value that signifies blank (missing or null) data in an integer image. * * @deprecated This is only applicable to {@link ImageHDU} or {@link RandomGroupsHDU} with integer * type data and not for other HDU or data types. * * @return the integer value used for identifying blank / missing data in integer images. * * @throws FitsException if the header does not specify a blanking value. */ public long getBlankValue() throws FitsException { if (!myHeader.containsKey(BLANK.key())) { throw new FitsException("BLANK undefined"); } return myHeader.getLongValue(BLANK); } /** * Returns the floating-point increment between adjacent integer values in the image. * * @deprecated This is only applicable to {@link ImageHDU} or {@link RandomGroupsHDU} with integer type data and not * for other HDU or data types. * * @return the floating-point quantum that corresponds to the increment of 1 in the integer data representation. * * @see #getBZero() */ @Deprecated public double getBScale() { return myHeader.getDoubleValue(BSCALE, 1.0); } /** * Returns the floating-point value that corresponds to an 0 integer value in the image. * * @deprecated This is only applicable to {@link ImageHDU} or {@link RandomGroupsHDU} with integer type data and not * for other HDU or data types. * * @return the floating point value that correspond to the integer 0 in the image data. * * @see #getBScale() */ @Deprecated public double getBZero() { return myHeader.getDoubleValue(BZERO, 0.0); } /** * Get the FITS file creation date as a Date object. * * @return either null or a Date object */ public Date getCreationDate() { try { return new FitsDate(myHeader.getStringValue(DATE)).toDate(); } catch (FitsException e) { LOG.log(Level.SEVERE, "Unable to convert string to FITS date", e); return null; } } /** * Returns the data component of this HDU. * * @return the associated Data object */ public DataClass getData() { return myData; } /** * Get the equinox in years for the celestial coordinate system in which positions given in either the header or * data are expressed. * * @return either null or a String object * * @deprecated use {@link #getEquinox()} instead */ @Deprecated public double getEpoch() { return myHeader.getDoubleValue(EPOCH, -1.0); } /** * Get the equinox in years for the celestial coordinate system in which positions given in either the header or * data are expressed. * * @return either null or a String object */ public double getEquinox() { return myHeader.getDoubleValue(EQUINOX, -1.0); } @Override public long getFileOffset() { return myHeader.getFileOffset(); } /** * Returns the number of data objects (of identical shape and size) that are group together in this HDUs data * segment. For most data types this would be simply 1, except for {@link RandomGroupsData}, where other values are * possible. * * @return the number of data objects (of identical shape and size) that are grouped together in the data * segment. * * @deprecated Should not be exposed outside of {@link RandomGroupsHDU} -- will reduce visibility in the future/ * * @see #getParameterCount() */ public int getGroupCount() { return myHeader.getIntValue(GCOUNT, 1); } /** * Returns the decoded checksum that is stored in the header of this HDU under the CHECKSUM keyword. It * does not have much use, and is not needed for integrity verification since the purpose of the CHECKSUM value is * merely to ensure that the checksum of the HDU is always (int) -1. * * @deprecated Not very useful, since it has no meaning other than ensuring that the checksum of the * HDU yields (int) -1 (that is 0xffffffff) after including * this value for the CHECKSUM keyword in the header. It will be removed in the * future. Use {@link #verifyIntegrity()} instead when appropriate. * * @return the decoded FITS checksum value recorded in the HDU * * @throws FitsException if the HDU's header does not contain a CHECKSUM keyword. * * @see #calcChecksum() * @see Fits#calcChecksum(int) * @see #getStoredDatasum() * @see FitsCheckSum#getStoredDatasum(Header) * * @since 1.17 */ public long getStoredChecksum() throws FitsException { return FitsCheckSum.getStoredChecksum(myHeader); } /** * Returns the FITS checksum for the HDU's data that is stored in the header of this HDU under the * DATASUM keyword. This may be useful to compare against the checksum calculated from data in memory * (e.g. via {@link Data#calcChecksum()}) to check changes / corruption of the in-memory data vs what was stored in * the file. Note however, that this type of checkum test will fail if the file used non-standard padding at the end * of the data segment, even if the data themselves are identical. Hence, for verifying data contained in a file * {@link #verifyDataIntegrity()} or {@link #verifyIntegrity()} should be preferred. * * @return the FITS DATASUM value recorded in the HDU * * @throws FitsException if the HDU's header does not contain a DATASUM keyword. * * @see #verifyDataIntegrity() * @see Data#calcChecksum() * * @since 1.17 */ public long getStoredDatasum() throws FitsException { return FitsCheckSum.getStoredDatasum(myHeader); } /** *

* Computes the checksums for this HDU and stores the CHECKSUM and DATASUM values in the * header. This should be the last modification to the HDU before writing it. *

*

* Note, that this method will always calculate the checksum in memory. As a result it will load data in deferred * read mode into RAM for performaing the calculation. If you prefer to keep deferred read mode data unloaded, you * should use {@link Fits#setChecksum(int)} instead. * * @throws FitsException if there was an error serializing the HDU for the checksum computation. * * @see Fits#setChecksum(int) * @see FitsCheckSum#setChecksum(BasicHDU) * @see #getStoredDatasum() * * @since 1.17 */ public void setChecksum() throws FitsException { FitsCheckSum.setChecksum(this); } /** * Checks the HDU's integrity, using the recorded CHECKSUM and/or DATASUM keywords if present. In addition of * performing the same checks as {@link #verifyDataIntegrity()}, it also checks the overall checksum of the HDU if * possible. When the header has a CHECKSUM keyword stored, the overall checksum of the HDU must be * 0xffffffff, that is -1 in 32-bit representation. * * @return true if the HDU has a CHECKSUM and/or DATASUM record to check against, * otherwise false * * @throws FitsException if the HDU fails the integrity test. * @throws IOException if there was an I/O error accessing the input. * * @see #verifyDataIntegrity() * @see Fits#verifyIntegrity() * * @since 1.18.1 */ @SuppressWarnings("resource") public boolean verifyIntegrity() throws FitsException, IOException { boolean result = verifyDataIntegrity(); if (myHeader.getCard(Checksum.CHECKSUM) == null) { return result; } long fsum = (myHeader.getStreamChecksum() < 0) ? FitsCheckSum.checksum(myHeader.getRandomAccessInput(), getFileOffset(), getSize()) : FitsCheckSum.sumOf(myHeader.getStreamChecksum(), myData.getStreamChecksum()); if (fsum != FitsCheckSum.HDU_CHECKSUM) { throw new FitsIntegrityException("checksum", fsum, FitsCheckSum.HDU_CHECKSUM); } return true; } /** * Checks that the HDUs data checksum is correct. The recorded DATASUM will be used, if available, to check the * integrity of the data segment. * * @return true if the HDU has DATASUM record to check against, otherwise * false * * @throws FitsException if the HDU fails the integrity test. * @throws IOException if there was an I/O error accessing the input. * * @see #verifyIntegrity() * @see Fits#verifyIntegrity() * * @since 1.18.1 */ @SuppressWarnings("resource") public boolean verifyDataIntegrity() throws FitsException, IOException { if (getHeader().getCard(Checksum.DATASUM) == null) { return false; } Data d = getData(); RandomAccess rin = myData.getRandomAccessInput(); long fsum = (rin != null) ? FitsCheckSum.checksum(rin, d.getFileOffset(), d.getSize()) : d.getStreamChecksum(); if (fsum != getStoredDatasum()) { throw new FitsIntegrityException("datasum", fsum, getStoredDatasum()); } return true; } /** * Computes and returns the FITS checksum for this HDU, e.g. to compare agains the stored CHECKSUM in * the FITS header. This method always computes the checksum from data fully loaded in memory. As such it will load * deferred read mode data into RAM to perform the calculation. If you prefer to leave the data in deferred read * mode, you can use {@link Fits#calcChecksum(int)} instead. * * @deprecated Use {@link #verifyIntegrity()} instead when appropriate. It's not particularly useful * since integrity checking does not use or require knowledge of this sum. May be * removed from future releases. * * @return the computed HDU checksum (in memory). * * @throws FitsException if there was an error while calculating the checksum * * @see Data#calcChecksum() * @see Fits#calcChecksum(int) * @see FitsCheckSum#checksum(BasicHDU) * * @since 1.17 */ public long calcChecksum() throws FitsException { return FitsCheckSum.checksum(this); } /** * Returns the FITS header component of this HDU * * @return the associated header * * @see Fits#getPrimaryHeader() * @see Fits#getCompleteHeader(int) * @see Fits#getCompleteHeader(String) * @see Fits#getCompleteHeader(String, int) */ public Header getHeader() { return myHeader; } /** * Returns a header card builder for filling the header cards using the builder pattern. * * @param key the key for the first card. * * @return the builder for header cards. */ public HeaderCardBuilder card(IFitsHeader key) { return myHeader.card(key); } /** * Get the name of the instrument which was used to acquire the data in this FITS file. * * @return either null or a String object */ public String getInstrument() { return myHeader.getStringValue(INSTRUME); } /** * Returns the underlying Java object (usually an array of some type) that stores the data internally. * * @return the non-FITS data object. Same as {@link #getData()}.getKernel(). */ public final Object getKernel() { try { return myData.getKernel(); } catch (FitsException e) { LOG.log(Level.SEVERE, "Unable to get kernel data", e); return null; } } /** * Return the minimum valid value in the array. * * @return minimum value. */ public double getMaximumValue() { return myHeader.getDoubleValue(DATAMAX); } /** * Return the minimum valid value in the array. * * @return minimum value. */ public double getMinimumValue() { return myHeader.getDoubleValue(DATAMIN); } /** * Get the name of the observed object in this FITS file. * * @return either null or a String object */ public String getObject() { return myHeader.getStringValue(OBJECT); } /** * Get the FITS file observation date as a Date object. * * @return either null or a Date object */ public Date getObservationDate() { try { return new FitsDate(myHeader.getStringValue(DATE_OBS)).toDate(); } catch (FitsException e) { LOG.log(Level.SEVERE, "Unable to convert string to FITS observation date", e); return null; } } /** * Get the name of the person who acquired the data in this FITS file. * * @return either null or a String object */ public String getObserver() { return myHeader.getStringValue(OBSERVER); } /** * Get the name of the organization which created this FITS file. * * @return either null or a String object */ public String getOrigin() { return myHeader.getStringValue(ORIGIN); } /** * Returns the number of parameter bytes (per data group) accompanying each data object in the group. * * @return the number of bytes used for arbitrary extra parameters accompanying each data object in the group. * * @deprecated Should not be exposed outside of {@link RandomGroupsHDU} -- will reduce visibility in the future. * * @see #getGroupCount() */ public int getParameterCount() { return myHeader.getIntValue(PCOUNT, 0); } /** * Return the citation of a reference where the data associated with this header are published. * * @return either null or a String object */ public String getReference() { return myHeader.getStringValue(REFERENC); } @Override public long getSize() { long size = 0; if (myHeader != null) { size += myHeader.getSize(); } if (myData != null) { size += myData.getSize(); } return size; } /** * Get the name of the telescope which was used to acquire the data in this FITS file. * * @return either null or a String object */ public String getTelescope() { return myHeader.getStringValue(TELESCOP); } /** * Get the String value associated with the header keyword. Trailing spaces are not significant in FITS * headers and are automatically omitted during parsing. Leading spaces are however considered significant, and are * retained otherwise. * * @param keyword the FITS keyword * * @deprecated (for internal use) Will reduced visibility in the future. Use * {@link Header#getStringValue(IFitsHeader)} or similar instead followed by * {@link String#trim()} if necessary. * * @return either null or a String with leading/trailing blanks stripped. */ public String getTrimmedString(String keyword) { String s = myHeader.getStringValue(keyword); if (s != null) { s = s.trim(); } return s; } /** * Get the String value associated with the header keyword.with leading spaces removed. Trailing spaces * are not significant in FITS headers and are automatically omitted during parsing. Leading spaces are however * considered significant, and are retained otherwise. * * @param keyword the FITS keyword * * @deprecated (for internal use) Will reduced visibility in the future. Use * {@link Header#getStringValue(String)} or similar instead followed by * {@link String#trim()} if necessary. * * @return either null or a String with leading/trailing blanks stripped. */ public String getTrimmedString(IFitsHeader keyword) { return getTrimmedString(keyword.key()); } /** * Print out some information about this HDU. * * @param stream the printstream to write the info on * * @throws FitsException if the HDU is malformed */ public abstract void info(PrintStream stream) throws FitsException; @Override @SuppressWarnings({"unchecked", "deprecation"}) public void read(ArrayDataInput stream) throws FitsException, IOException { setHeader(Header.readHeader(stream)); myData = (DataClass) FitsFactory.dataFactory(myHeader); myData.read(stream); } @Override public boolean reset() { return myHeader.reset(); } @Override public void rewrite() throws FitsException, IOException { if (!rewriteable()) { throw new FitsException("Invalid attempt to rewrite HDU"); } myHeader.rewrite(); if (!myData.isDeferred()) { myData.rewrite(); } } @Override public boolean rewriteable() { return myHeader.rewriteable() && myData.rewriteable(); } /** * Indicate that an HDU is the first element of a FITS file. * * @param value value to set * * @throws FitsException if the operation failed */ void setPrimaryHDU(boolean value) throws FitsException { if (value && !canBePrimary()) { throw new FitsException("Invalid attempt to make HDU of type:" + this.getClass().getName() + " primary."); } Header.KeywordCheck mode = myHeader.getKeywordChecking(); myHeader.setKeywordChecking(Header.KeywordCheck.DATA_TYPE); myHeader.setRequiredKeys(value ? null : getCanonicalXtension()); myHeader.setKeywordChecking(mode); } /** * Returns the canonical (expected) value for the XTENSION keywords for this type of HDU. Concrete HDU * implementations should override this method as appropriate. As of FITS version 4, only the following XTENSION * values are recognised: 'IMAGE', 'TABLE', and 'BINTABLE'. * * @return The value to use for the XTENSION keyword. * * @since 1.18 */ protected String getCanonicalXtension() { // TODO this should become an abstract method for 2.0. Prior to that we provide a default // implementation for API back-compatibility reasons for any 3rd-party HDU implementations. // To warn that this should be ovewritten, we'll log a warning... LOG.warning(getClass().getName() + " should override getCanonicalXtension() method as appropriate."); return "UNKNOWN"; } @Override public void write(ArrayDataOutput stream) throws FitsException { if (myHeader == null) { setHeader(new Header()); } if (stream instanceof FitsOutput) { boolean isFirst = ((FitsOutput) stream).isAtStart(); setPrimaryHDU(canBePrimary() && isFirst); } myHeader.write(stream); if (myData != null) { myData.write(stream); } try { stream.flush(); } catch (IOException e) { throw new FitsException("Error flushing at end of HDU", e); } } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/BinaryTable.java000066400000000000000000005332121476377620500241470ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; import java.text.DecimalFormat; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.StringTokenizer; import java.util.logging.Logger; import nom.tam.fits.header.Bitpix; import nom.tam.fits.header.NonStandard; import nom.tam.fits.header.Standard; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.ArrayFuncs; import nom.tam.util.AsciiFuncs; import nom.tam.util.ColumnTable; import nom.tam.util.ComplexValue; import nom.tam.util.Cursor; import nom.tam.util.FitsEncoder; import nom.tam.util.FitsIO; import nom.tam.util.Quantizer; import nom.tam.util.RandomAccess; import nom.tam.util.ReadWriteAccess; import nom.tam.util.TableException; import nom.tam.util.type.ElementType; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Table data for binary table HDUs. It has been thoroughly re-written for 1.18 to improve consistency, increase * performance, make it easier to use, and to enhance. * * @see BinaryTableHDU * @see AsciiTable */ @SuppressWarnings("deprecation") public class BinaryTable extends AbstractTableData implements Cloneable { /** For fixed-length columns */ private static final char POINTER_NONE = 0; /** FITS 32-bit pointer type for variable-sized columns */ private static final char POINTER_INT = 'P'; /** FITS 64-bit pointer type for variable-sized columns */ private static final char POINTER_LONG = 'Q'; /** Shape firs singleton / scalar entries */ private static final int[] SINGLETON_SHAPE = new int[0]; /** The substring convention marker */ private static final String SUBSTRING_MARKER = ":SSTR"; /** * Describes the data type and shape stored in a binary table column. */ public static class ColumnDesc implements Cloneable { private boolean warnedFlatten; /** byte offset of element from row start */ private int offset; /** The number of primitive elements in the column */ private int fitsCount; /** The dimensions of the column */ private int[] fitsShape = SINGLETON_SHAPE; /** Shape on the Java side. Differs from the FITS TDIM shape for String and complex values. */ private int[] legacyShape = SINGLETON_SHAPE; /** Length of string elements */ private int stringLength = -1; /** The class array entries on the Java side. */ private Class base; /** The FITS element class associated with the column. */ private Class fitsBase; /** Heap pointer type actually used for locating variable-length column data on the heap */ private char pointerType; /** * String component delimiter for substring arrays, for example as defined by the TFORM keyword that uses the * substring array convention... */ private byte delimiter; /** * Is this a complex column. Each entry will be associated with a float[2] or double[2] */ private boolean isComplex; /** * Whether this column contains bit arrays. These take up to 8-times less space than logicals, which occupy a * byte per value. */ private boolean isBits; /** * User defined column name */ private String name; private Quantizer quant; /** * Creates a new column descriptor with default settings and 32-bit integer heap pointers. */ protected ColumnDesc() { } /** * Creates a new column descriptor with default settings, and the specified type of heap pointers * * @param type The Java type of base elements that this column is designated to contain. For example * int.class if the column will contain integers or arrays of integers. * * @throws FitsException if the base type is not one that can be used in binary table columns. */ private ColumnDesc(Class type) throws FitsException { this(); base = type; if (base == boolean.class) { fitsBase = byte.class; isBits = true; } else if (base == Boolean.class) { base = boolean.class; fitsBase = byte.class; } else if (base == String.class) { fitsBase = byte.class; } else if (base == ComplexValue.class) { base = double.class; fitsBase = double.class; isComplex = true; } else if (base == ComplexValue.Float.class) { base = float.class; fitsBase = float.class; isComplex = true; } else if (base.isPrimitive()) { fitsBase = type; if (base == char.class && FitsFactory.isUseUnicodeChars()) { LOG.warning("char[] will be written as 16-bit integers (type 'I'), not as a ASCII bytes (type 'A')" + " in the binary table. If that is not what you want, you should set FitsFactory.setUseUnicodeChars(false)."); LOG.warning( "Future releases will disable Unicode support by default as it is not supported by the FITS standard." + " If you do want it still, use FitsFactory.setUseUnicodeChars(true) explicitly to keep the non-standard " + " behavior as is."); } } else { throw new TableException("Columns of type " + base + " are not supported."); } } /** * Creates a new column descriptor for the specified boxed Java type, and fixed array shape. The type may be any * primitive type, or else String.class, Boolean.class (for FITS logicals), * ComplexValue.class or ComplexValue.Float.class (for complex values with 64-bit and * 32-bit precision, respectively). Whereas {@link Boolean} type columns will be stored as FITS logicals (1 * element per byte), boolean types will be stored as packed bits (with up to 8 bits per byte). * * @param base The Java type of base elements that this column is designated to contain. For example * int.class if the column will contain integers or arrays of integers. * @param dim the fixed dimensions of the table entries. For strings the trailing dimension must * specify the fixed length of strings. * * @throws FitsException if the base type is not one that can be used in binary table columns. * * @see #createForScalars(Class) * @see #createForStrings(int) * @see #createForStrings(int, int[]) * @see #createForVariableSize(Class) * * @since 1.18 */ public ColumnDesc(Class base, int... dim) throws FitsException { this(base); setBoxedShape(dim); } /** * Sets a user-specified name for this column. The specified name will be used as the TTYPEn value for this * column. * * @param value The new name for this column. * * @return itself, to support builder patterns. * * @throws IllegalArgumentException If the name contains characters outside of the ASCII range of 0x20 - 0x7F * allowed by FITS. * * @see #name() * @see #getDescriptor(String) * @see #indexOf(String) * @see #addColumn(ColumnDesc) * * @since 1.20 * * @author Attila Kovacs */ public ColumnDesc name(String value) throws IllegalArgumentException { HeaderCard.validateChars(value); this.name = value; return this; } /** * Returns the name of this column, as it was stored or would be stored by a TTYPEn value in the FITS header. * * @return the name of this column * * @see #name(String) * * @since 1.20 * * @author Attila Kovacs */ public String name() { return this.name; } /** * Returns the conversion between decimal and integer data representations for the column data. * * @return the quantizer that converts between floating-point and integer data representations, which may be * null. * * @see #setQuantizer(Quantizer) * * @since 1.20 */ public Quantizer getQuantizer() { return quant; } /** * Sets the conversion between decimal and integer data representations for the column data. If the table is * read from a FITS input, the column's quantizer is automatically set if the Table HDU's header defines any of * the TSCALn, TZEROn, or TNULLn keywords for the column. Users can override that by specifying another quatizer * to use for the column, or dicard qunatizing by calling this method with a nullargument. * * @param q the quantizer that converts between floating-point and integer data representations, or * null to not use any quantization, and instead rely on the generic rounding for * decimal-integer conversions for this column. * * @see #getQuantizer() * * @since 1.20 */ public void setQuantizer(Quantizer q) { this.quant = q; } /** * Recalculate the FITS element count based on the shape of the data */ private void calcFitsCount() { fitsCount = 1; for (int size : fitsShape) { fitsCount *= size; } } /** * Sets the shape of entries in the older Java array format of this library (used exclusively prior to 1.18). * For complex columns, there is an extra [2] dimension, such that single complex values are stored * as an array of [2], and an array of n complex values are stored as arrays of * [n][2]. Otherwise it's the same as {@link #setBoxedShape(int...)}. * * @param dim The Java dimensions for legacy arrays, such as returned by * {@link BinaryTable#getElement(int, int)} */ private void setLegacyShape(int... dim) { legacyShape = dim; calcFitsShape(); calcFitsCount(); } /** * Sets the shape of entries as stored in the FITS header by hte TDIM keyword. * * @param dim The dimensions for the TDIM keyword in Java order (outer dimensions first), which is the reverse * of FITS order (inner-dimensions first). */ private void setFitsShape(int... dim) { fitsShape = dim; calcLegacyShape(); calcFitsCount(); } /** * Sets the shape of boxed Java array entries. For complex columns, all single entries, including strings and * complex values, have scalar shape [], whereas an array of n have shape [n]. * * @param dim The Java dimensions for legacy arrays, such as returned by * {@link BinaryTable#getElement(int, int)} */ private void setBoxedShape(int... dim) { if (isComplex()) { setFitsShape(dim); } else { setLegacyShape(dim); } } /** * Returns the maximum length of string elements contained in this colum, or -1 if this is not a string based * column, or if there is no limit set to string size (e.g. in a variable-length column) * * @return the maximum length of string values stored in this column, or -1 if it is not as string column, or if * its a string column containing strings of unconstrained variable length. * * @see #getEntryShape() * * @since 1.18 */ public final int getStringLength() { return stringLength; } /** * Sets the maximum length of string elements in this column. * * @param len The fixed string length in bytes. */ private void setStringLength(int len) { stringLength = len; if (!isVariableSize()) { calcFitsShape(); calcFitsCount(); } } /** * Returns the string delimiter that separates packed substrings in variable-length string arrays. * * @return the delimiter byte value (usually between 0x20 and 0x7e) or 0 if no delimiter was set. * * @see #getStringLength() */ public final byte getStringDelimiter() { return delimiter; } /** * Creates a new column descriptor for a non-string based scalar column. The type may be any primitive type, or * Boolean.class (for FITS logicals), ComplexValue.class or * ComplexValue.Float.class (for complex values with 64-bit and 32-bit precision, respectively). * Whereas {@link Boolean} type columns will be stored as FITS logicals (1 element per byte), * boolean types will be stored as packed bits (with up to 8 bits per byte). * * @param type The Java type of base elements that this column is designated to contain. * For example int.class if the column will contain integers * or arrays of integers. It must not be String.class. To * create scalar {@link String} columns use {@link #createForStrings(int)} * instead. * * @return the new column descriptor. * * @throws IllegalArgumentException if the type is String.class, for which you should be using * {@link #createForStrings(int)} instead. * @throws FitsException if the base type is not one that can be used in binary table columns. * * @see #createForFixedArrays(Class, int[]) * @see #createForVariableSize(Class) * * @since 1.18 */ public static ColumnDesc createForScalars(Class type) throws IllegalArgumentException, FitsException { if (String.class.isAssignableFrom(type)) { throw new IllegalArgumentException("Use the createStrings(int) method for scalar strings."); } return new ColumnDesc(type, SINGLETON_SHAPE); } /** * Creates a new column descriptor for fixed-shape non-string arrays. The type may be any primitive type, or * else Boolean.class (for FITS logicals), ComplexValue.class or * ComplexValue.Float.class (for complex values with 64-bit and 32-bit precision, respectively). * Whereas {@link Boolean} type columns will be stored as FITS logicals (1 element per byte), * boolean types will be stored as packed bits (with up to 8 bits per byte). * * @param type The Java type of base elements that this column is designated to contain. * For example int.class if the column will contain integers * or arrays of integers. It must not be String.class. To * create scalar {@link String} columns use {@link #createForStrings(int)} * instead. * @param dim the fixed dimensions of the table entries. For strings the trailing * dimension must specify the fixed length of strings. * * @return the new column descriptor. * * @throws IllegalArgumentException if the type is String.class, for which you should be using * {@link #createForStrings(int, int[])} instead. * @throws FitsException if the base type is not one that can be used in binary table columns. * * @see #createForScalars(Class) * @see #createForStrings(int) * @see #createForStrings(int, int[]) * @see #createForVariableSize(Class) * * @since 1.18 */ public static ColumnDesc createForFixedArrays(Class type, int... dim) throws IllegalArgumentException, FitsException { if (String.class.isAssignableFrom(type)) { throw new IllegalArgumentException("Use the createStrings(int) method for scalar strings."); } return new ColumnDesc(type, dim); } /** * Creates a new column descriptor for single string entries of fixed maximum length. * * @param len The fixed string length in bytes. * * @return the new column descriptor * * @throws FitsException if the base type is not one that can be used in binary table columns. * * @see #createForScalars(Class) * @see #createForStrings(int, int[]) * * @since 1.18 */ public static ColumnDesc createForStrings(int len) throws FitsException { return createForStrings(len, SINGLETON_SHAPE); } /** * Creates a new column descriptor for arrays of string entries of fixed maximum length. * * @param len The fixed string length in bytes. * @param outerDims The shape of string arrays * * @return the new column descriptor * * @throws FitsException if the base type is not one that can be used in binary table columns. * * @see #createForVariableStringArrays(int) * @see #createForStrings(int) * * @since 1.18 */ public static ColumnDesc createForStrings(int len, int... outerDims) throws FitsException { ColumnDesc c = new ColumnDesc(String.class); c.setLegacyShape(outerDims); c.setStringLength(len); return c; } /** * Creates a new column descriptor for variable-length arrays of fixed-length string entries. Each string * component will occupy exactly len bytes. * * @param len The fixed string storage length in bytes. * * @return the new column descriptor * * @throws FitsException if the column could not be created. * * @see #createForDelimitedStringArrays(byte) * @see #createForStrings(int, int[]) * * @since 1.18 */ public static ColumnDesc createForVariableStringArrays(int len) throws FitsException { ColumnDesc c = createForVariableSize(String.class); c.setStringLength(len); return c; } /** * Creates a new column descriptor for variable-length arrays of delimited string entries. * * @param delim the byte value that delimits strings that are shorter than the storage length. It * should be in the ASCII range of 0x20 through 0x7e. * * @return the new column descriptor * * @throws FitsException if the column could not be created. * * @see #createForDelimitedStringArrays(byte) * @see #createForStrings(int, int[]) * * @since 1.18 */ public static ColumnDesc createForDelimitedStringArrays(byte delim) throws FitsException { ColumnDesc c = createForVariableStringArrays(-1); c.setStringDelimiter(delim); return c; } /** * Creates a new column descriptor for variable length 1D arrays or strings. The type may be any primitive type, * or else String.class, Boolean.class (for FITS logicals), * ComplexValue.class or ComplexValue.Float.class (for complex values with 64-bit and * 32-bit precision, respectively). Whereas {@link Boolean} type columns will be stored as FITS logicals (1 * element per byte), boolean types will be stored as packed bits (with up to 8 elements per byte). * * @param type The Java type of base elements that this column is designated to contain. For example * int.class if the column will contain integers or arrays of integers. * * @return the new column descriptor * * @throws FitsException if the base type is not one that can be used in binary table columns. * * @see #createForScalars(Class) * @see #createForStrings(int) * @see #createForStrings(int, int[]) * @see #ColumnDesc(Class, int[]) * * @since 1.18 */ public static ColumnDesc createForVariableSize(Class type) throws FitsException { ColumnDesc c = new ColumnDesc(type); c.setVariableSize(false); return c; } /** * Recalculate the legacy Java entry shape from the FITS shape (as stored by TDIM). Strings drop the last * dimension from the FITS shape (which becomes the string length), while complex values add a dimension of * [2] to the FITS shape, reflecting the shape of their real-valued components. */ private void calcLegacyShape() { if (isString()) { legacyShape = Arrays.copyOf(fitsShape, fitsShape.length - 1); stringLength = fitsShape[fitsShape.length - 1]; } else if (isComplex()) { legacyShape = Arrays.copyOf(fitsShape, fitsShape.length + 1); legacyShape[fitsShape.length] = 2; } else { legacyShape = fitsShape; } } /** * Recalculate the FITS storage shape (as reported by TDIM) from the legacy Java array shape */ private void calcFitsShape() { if (isString()) { fitsShape = Arrays.copyOf(legacyShape, legacyShape.length + 1); fitsShape[legacyShape.length] = stringLength; } else if (isComplex()) { fitsShape = Arrays.copyOf(legacyShape, legacyShape.length - 1); } else { fitsShape = legacyShape; } } /** * Returns the size of table entries in their trailing dimension. * * @return the number of elemental components in the trailing dimension of table entries. * * @see #getLeadingShape() */ private int getLastFitsDim() { return fitsShape[fitsShape.length - 1]; } @Override public ColumnDesc clone() { try { ColumnDesc copy = (ColumnDesc) super.clone(); fitsShape = fitsShape.clone(); legacyShape = legacyShape.clone(); // Model should not be changed... return copy; } catch (CloneNotSupportedException e) { return null; } } /** * Specifies that this columns contains single (not array) boxed entrie, such as single primitives, strings, or * complex values. */ private void setSingleton() { setBoxedShape(SINGLETON_SHAPE); } /** * Checks if this column contains single (scalar / non-array) elements only, including single strings or single * complex values. * * @return true if the column contains individual elements of its type, or else false * if it contains arrays. * * @since 1.18 */ public final boolean isSingleton() { if (isVariableSize()) { return isString() ? (stringLength < 0 && delimiter == 0) : false; } if (isComplex()) { return fitsShape.length == 0; } return legacyShape.length == 0; } /** * Checks if this column contains logical values. FITS logicals can each hve true, * false or null (undefined) values. It is the support for these undefined values that * set it apart from typical booleans. Also, logicals are stored as one byte per element. So if using only * true, false values without null bits will offer more compact storage * (by up to a factor of 8). You can convert existing logical columns to bits via * {@link BinaryTable#convertToBits(int)}. * * @return true if this column contains logical values. * * @see #isBits() * @see BinaryTable#convertToBits(int) * * @since 1.18 */ public final boolean isLogical() { return base == Boolean.class || (base == boolean.class && !isBits); } /** * Checks if this column contains only true boolean values (bits). Unlike logicals, bits can have only * true, false values with no support for null , but offer more compact * storage (by up to a factor of 8) than logicals. You can convert existing logical columns to bits via * {@link BinaryTable#convertToBits(int)}. * * @return true if this column contains true / false bits only. * * @see #isLogical() * @see BinaryTable#convertToBits(int) * * @since 1.18 */ public final boolean isBits() { return base == boolean.class && isBits; } /** * Checks if this column stores ASCII strings. * * @return true if this column contains only strings. * * @see #isVariableSize() */ public final boolean isString() { return base == String.class; } /** * Checks if this column contains complex values. You can convert suitable columns of float or * double elements to complex using {@link BinaryTable#setComplexColumn(int)}, as long as the last * dimension is 2, ir if the variable-length columns contain even-number of values exclusively. * * @return true if this column contains complex values. */ public final boolean isComplex() { return isComplex; } /** * Checks if this column contains numerical values, such as any primitive number type (e.g. * nt.class or double.class) or else a {@link ComplexValue} type. type. * * @return true if this column contains numerical data, including complex-valued data. String, * bits, and FITS logicals are not numerical (but all other column types are). * * @since 1.20 */ public final boolean isNumeric() { return !isLogical() && !isBits() && !isString(); } /** * Returns the Java array element type that is used in Java to represent data in this column. When accessing * columns or their elements in the old way, through arrays, this is the type that arrays from the Java side * will expect or provide. For example, when storing {@link String} values (regular or variable-sized), this * will return String.class. Arrays returned by {@link BinaryTable#getColumn(int)}, * {@link BinaryTable#getRow(int)}, and {@link BinaryTable#getElement(int, int)} will return arrays of this * type, and the equivalent methods for setting data will expect arrays of this type as their argument. * * @return the Java class, arrays of which, packaged data for this column on the Java side. * * @deprecated Ambiguous, use {@link #getLegacyBase()} instead. It can be confusing since it is not clear if it * refers to array element types used in FITS storage or on the java side when using the older * array access, or if it refers to the class of entries in the main table, which may be heap * pointers. It is also distinct from {@link #getElementClass()}, which returns the boxed type * used by {@link BinaryTable#get(int, int)} or {@link BinaryTable#set(int, int, Object)}. */ public Class getBase() { return getLegacyBase(); } /** * Returns the primitive type that is used to store the data for this column in the FITS representation. This is * the class for the actual data type, whether regularly shaped (multidimensional) arrays or variable length * arrays (on the heap). For example, when storing {@link String} values (regular or variable-sized), this will * return byte.class. * * @return the primitive class, in used for storing data in the FITS representation. * * @see #getLegacyBase() * * @since 1.18 */ final Class getFitsBase() { return fitsBase; } /** *

* Returns the Java array element type that is used in Java to represent data in this column for the legacy * table access methods. When accessing columns or their elements in the old way, through arrays, this is the * type that arrays from the Java side will expect or provide. For example, when storing complex values (regular * or variable-sized), this will return float.class or double.class. Arrays returned * by {@link BinaryTable#getColumn(int)}, {@link BinaryTable#getRow(int)}, and * {@link BinaryTable#getElement(int, int)} will return arrays of this type. *

*

* This is different from {@link #getElementClass()}, which in turn returns the boxed type of objects returned * by {@link BinaryTable#get(int, int)}. * * @return the Java class, arrays of which, packaged data for this column on the Java side. * * @see #getElementClass() * * @since 1.18 */ public Class getLegacyBase() { return base; } /** * (for internal use) Returns the primitive data class which is used for storing entries in the main * (regular) table. For variable-sized columns, this will be the heap pointer class, not the FITS data class. * * @return the class in which main table entries are stored. * * @see #isVariableSize() */ private Class getTableBase() { return isVariableSize() ? pointerClass() : getFitsBase(); } /** * Returns the dimensions of elements in this column. As of 1.18, this method returns a copy ot the array used * internally, which is safe to modify. * * @return an array with the element dimensions. * * @deprecated (for internal use) Use {@link #getEntryShape()} instead. Not useful to users since it * returns the dimensions of the primitive storage types, which is not always the dimension of * table entries on the Java side. */ public int[] getDimens() { return fitsShape.clone(); } /** * (for internal use) The dimension of elements in the FITS representation. * * @return the dimension of elements in the FITS representation. For example an array of string will be 2 * (number of string, number of bytes per string). * * @see #getEntryDimension() */ private int fitsDimension() { return fitsShape.length; } /** * Returns the boxed Java type of elements stored in a column. * * @return The java type of elements in the columns. For columns containing strings, FITS logicals, or complex * values it will be String.class, Boolean.class or * ComplexValue.class respectively. For all other column types the primitive class of * the elements contained (e.g. char.class, float.class) is returned. * * @since 1.18 * * @see ColumnDesc#getElementCount() * @see ColumnDesc#getEntryShape() * @see ColumnDesc#getLegacyBase() */ public final Class getElementClass() { if (isLogical()) { return Boolean.class; } if (isComplex()) { return ComplexValue.class; } return base; } /** * Returns the dimensionality of the 'boxed' elements as returned by {@link BinaryTable#get(int, int)} or * expected by {@link BinaryTable#set(int, int, Object)}. That is it returns the dimnesion of 'boxed' elements, * such as strings or complex values, rather than the dimension of characters or real components stored in the * FITS for these. * * @return the number of array dimensions in the 'boxed' Java type for this column. Variable-sized columns will * always return 1. * * @see #getEntryShape() * @see #getElementCount() * * @since 1.18 */ public final int getEntryDimension() { if (isVariableSize()) { return 1; } return isString() ? legacyShape.length : fitsShape.length; } /** * Returns the array shape of the 'boxed' elements as returned by {@link BinaryTable#get(int, int)} or expected * by {@link BinaryTable#set(int, int, Object)}. That is it returns the array shape of 'boxed' elements, such as * strings or complex values, rather than the shape of characters or real components stored in the FITS for * these. * * @return the array sized along each of the dimensions in the 'boxed' Java type for this column, or * null if the data is stored as variable-sized one-dimensional arrays of the boxed * element type. (Note, that accordingly variable-length string columns containing single strings * will thus return {1}, not null). * * @see #getEntryShape() * @see #getElementCount() * @see #isVariableSize() * * @since 1.18 */ public final int[] getEntryShape() { if (isVariableSize()) { return null; } if (isComplex) { return fitsShape.clone(); } return legacyShape.clone(); } /** * Returns the number of primitive elements (sych as bytes) that constitute a Java element (such as a String) in * this table. * * @return The number of primitives per Java element in the column, that is 1 for columns of primitive types, 2 * for complex-valued columns, or the number of bytes (characters) in a String element. * Variable-length strings will return -1. * * @since 1.18 * * @see #getElementCount() * @see #getLegacyBase() */ public final int getElementWidth() { if (isComplex()) { return 2; } if (isString()) { return getStringLength(); } return 1; } /** * Returns the number of 'boxed' elements as returned by {@link BinaryTable#get(int, int)} or expected by * {@link BinaryTable#set(int, int, Object)}. That is it returns the number of strings or complex values per * table entry, rather than the number of of characters or real components stored in the FITS for these. * * @return the number of array elements in the 'boxed' Java type for this column, or -1 if the column contains * elements of varying size. * * @see #getEntryShape() * @see #getEntryDimension() * @see #isVariableSize() * * @since 1.18 */ public final int getElementCount() { if (isVariableSize()) { return isString() ? 1 : -1; } if (isString()) { return fitsCount / getStringLength(); } return fitsCount; } /** * Returns the number of primitive base elements for a given FITS element count. * * @param fitsLen the FITS element count, sucj a a number of integers, complex-values, or bits * * @return the number of Java primitives that will be used to represent the number of FITS values for * this type of column. * * @see #getFitsBase() */ private int getFitsBaseCount(int fitsLen) { if (isBits) { return (fitsLen + Byte.SIZE - 1) / Byte.SIZE; } if (isComplex) { return fitsLen << 1; } return fitsLen; } /** * Returns the number of regular primitive table elements in this column. For example, variable-length columns * will always return 2, and complex-valued columns will return twice the number of complex values stored in * each table entry. * * @return the number of primitive table elements * * @since 1.18 */ public final int getTableBaseCount() { if (isVariableSize()) { return 2; } return getFitsBaseCount(fitsCount); } /** * Checks if this column contains entries of different size. Data for variable length coulmns is stored on the * heap as one-dimemnsional arrays. As such information about the 'shape' of data is lost when they are stored * that way. * * @return true if the column contains elements of variable size, or else false if all * entries have the same size and shape. */ public final boolean isVariableSize() { return pointerType != POINTER_NONE; } /** * @deprecated (for internal use) This method should be private in the future. * * @return new instance of the array with space for the specified number of rows. * * @param nRow the number of rows to allocate the array for */ public Object newInstance(int nRow) { return ArrayFuncs.newInstance(getTableBase(), getTableBaseCount() * nRow); } /** * @deprecated (for internal use) It may be reduced to private visibility in the future. Returns the * number of bytes that each element occupies in its FITS serialized form in the stored row * data. * * @return the number of bytes an element occupies in the FITS binary table data representation */ public int rowLen() { return getTableBaseCount() * ElementType.forClass(getTableBase()).size(); } /** * Checks if this column used 64-bit heap pointers. * * @return true if the column uses 64-bit heap pointers, otherwise false * * @see #createForVariableSize(Class) * * @since 1.18 */ public boolean hasLongPointers() { return pointerType == POINTER_LONG; } /** * Returns the TFORMn character code for the heap pointers in this column or 0 if this is * not a variable-sized column. * * @return int.class or long.class */ private char pointerType() { return pointerType; } /** * Returns the primitive class used for sotring heap pointers for this column * * @return int.class or long.class */ private Class pointerClass() { return pointerType == POINTER_LONG ? long.class : int.class; } /** * Sets whether this column will contain variable-length data, rather than fixed-shape data. * * @param useLongPointers true to use 64-bit heap pointers for variable-length arrays or else * false to use 32-bit pointers. */ private void setVariableSize(boolean useLongPointers) { pointerType = useLongPointers ? POINTER_LONG : POINTER_INT; fitsCount = 2; fitsShape = new int[] {2}; legacyShape = fitsShape; stringLength = -1; } /** * Sets a custom substring delimiter byte for variable length string arrays, between ASCII 0x20 and 0x7e. We * will however tolerate values outside of that range, but log an appropriate warning to alert users of the * violation of the standard. User's can either 'fix' it, or suppress the warning if they want to stick to their * guns. * * @param delim the delimiter byte value, between ASCII 0x20 and 0x7e (inclusive). * * @since 1.18 */ private void setStringDelimiter(byte delim) { if (delim < FitsUtil.MIN_ASCII_VALUE || delim > FitsUtil.MAX_ASCII_VALUE) { LOG.warning("WARNING! Substring terminator byte " + (delim & FitsIO.BYTE_MASK) + " outside of the conventional range of " + FitsUtil.MIN_ASCII_VALUE + " through " + FitsUtil.MAX_ASCII_VALUE + " (inclusive)"); } delimiter = delim; } /** * Checks if null array elements are permissible for this column. It is for strings (which map to * empty strings), and for logical columns, where they signify undefined values. * * @return true if null entries are considered valid for this column. */ private boolean isNullAllowed() { return isLogical() || isString(); } /** * Parses the substring array convention from a TFORM value, to set string length (if desired) and a delimiter * in variable-length string arrays. * * @param tform the TFORM header value for this column * @param pos the parse position immediately after the 'A' * @param setLength Whether to use the substring definition to specify the max string component length, for * example because it is not defined otherwise by TDIM. */ private void parseSubstringConvention(String tform, ParsePosition pos, boolean setLength) { if (setLength) { // Default string length... setStringLength(isVariableSize() ? -1 : fitsCount); } // Parse substring array convention... if (pos.getIndex() >= tform.length()) { return; } // Try 'rAw' format... try { int len = AsciiFuncs.parseInteger(tform, pos); if (setLength) { setStringLength(len); } return; } catch (Exception e) { // Keep going... } // Find if and where is the ":SSTR" marker in the format int iSub = tform.indexOf(SUBSTRING_MARKER, pos.getIndex()); if (iSub < 0) { // No substring definition... return; } pos.setIndex(iSub + SUBSTRING_MARKER.length()); // Set the substring width.... try { int len = AsciiFuncs.parseInteger(tform, pos); if (setLength) { setStringLength(len); } } catch (Exception e) { LOG.warning("WARNING! Could not parse substring length from TFORM: [" + tform + "]"); } // Parse substring array convention... if (pos.getIndex() >= tform.length()) { return; } if (AsciiFuncs.extractChar(tform, pos) != '/') { return; } try { setStringDelimiter((byte) AsciiFuncs.parseInteger(tform, pos)); } catch (NumberFormatException e) { // Warn if the delimiter is outside of the range supported by the convention. LOG.warning("WARNING! Could not parse substring terminator from TFORM: [" + tform + "]"); } } private void appendSubstringConvention(StringBuffer tform) { if (getStringLength() > 0) { tform.append(SUBSTRING_MARKER); tform.append(getStringLength()); if (delimiter != 0) { tform.append('/'); tform.append(new DecimalFormat("000").format(delimiter & FitsIO.BYTE_MASK)); } } } /** * Returns the TFORM header value to use for this column. * * @return The TFORM value that describes this column * * @throws FitsException If the column itself is invalid. */ String getTFORM() throws FitsException { StringBuffer tform = new StringBuffer(); tform.append(isVariableSize() ? "1" + pointerType() : fitsCount); if (base == int.class) { tform.append('J'); } else if (base == short.class) { tform.append('I'); } else if (base == byte.class) { tform.append('B'); } else if (base == char.class) { if (FitsFactory.isUseUnicodeChars()) { tform.append('I'); } else { tform.append('A'); } } else if (base == float.class) { tform.append(isComplex() ? 'C' : 'E'); } else if (base == double.class) { tform.append(isComplex() ? 'M' : 'D'); } else if (base == long.class) { tform.append('K'); } else if (isLogical()) { tform.append('L'); } else if (isBits()) { tform.append('X'); } else if (isString()) { tform.append('A'); if (isVariableSize()) { appendSubstringConvention(tform); } } else { throw new FitsException("Invalid column data class:" + base); } return tform.toString(); } /** * Returns the TDIM header value that descrives the shape of entries in this column * * @return the TDIM header value to use, or null if this column is not suited for a TDIM entry for * example because it is variable-sized, or because its entries are not multidimensional. . */ String getTDIM() { if (isVariableSize()) { return null; } if (fitsShape.length < 2) { return null; } StringBuffer tdim = new StringBuffer(); char prefix = '('; for (int i = fitsShape.length - 1; i >= 0; i--) { tdim.append(prefix); tdim.append(fitsShape[i]); prefix = ','; } tdim.append(')'); return tdim.toString(); } private boolean setFitsType(char type) throws FitsException { switch (type) { case 'A': fitsBase = byte.class; base = String.class; break; case 'X': fitsBase = byte.class; base = boolean.class; break; case 'L': fitsBase = byte.class; base = boolean.class; break; case 'B': fitsBase = byte.class; base = byte.class; break; case 'I': fitsBase = short.class; base = short.class; break; case 'J': fitsBase = int.class; base = int.class; break; case 'K': fitsBase = long.class; base = long.class; break; case 'E': case 'C': fitsBase = float.class; base = float.class; break; case 'D': case 'M': fitsBase = double.class; base = double.class; break; default: return false; } return true; } } /** * The enclosing binary table's properties * * @deprecated (for internal use) no longer used, and will be removed in the future. */ protected static class SaveState { /** * Create a new saved state * * @param columns the column descriptions to save * @param heap the heap to save * * @deprecated (for internal use) no longer in use. Will remove in the future. */ public SaveState(List columns, FitsHeap heap) { } } /** * Our own Logger instance, for nothing various non-critical issues. */ private static final Logger LOG = Logger.getLogger(BinaryTable.class.getName()); /** * This is the area in which variable length column data lives. */ private FitsHeap heap; /** * The heap start from the head of the HDU */ private long heapAddress; /** * (bytes) Empty space to leave after the populated heap area for future additions. */ private int heapReserve; /** * The original heap size (from the header) */ private int heapFileSize; /** * A list describing each of the columns in the table */ private List columns; /** * The number of rows in the table. */ private int nRow; /** * The length in bytes of each row. */ private int rowLen; /** * Where the data is actually stored. */ private ColumnTable table; private FitsEncoder encoder; /** * Creates an empty binary table, which can be populated with columns / rows as desired. */ public BinaryTable() { table = new ColumnTable<>(); columns = new ArrayList<>(); heap = new FitsHeap(0); nRow = 0; rowLen = 0; } /** * Creates a binary table from an existing column table. WARNING!, as of 1.18 we no longer use the column * data extra state to carry information about an enclosing class, because it is horribly bad practice. You should * not use this constructor to create imperfect copies of binary tables. Rather, use {@link #copy()} if you want to * create a new binary table, which properly inherits ALL of the properties of an original one. As for this * constructor, you should assume that it will not use anything beyond what's available in any generic vanilla * column table. * * @param tab the column table to create the binary table from. It must be a regular column table * that contains regular data of scalar or fixed 1D arrays only (not heap pointers). * No information beyond what a generic vanilla column table provides will be used. * Column tables don't store imensions for their elements, and don't have * variable-sized entries. Thus, if the table was the used in another binary table to * store flattened multidimensional data, we'll detect that data as 1D arrays. Andm if * the table was used to store heap pointers for variable length arrays, we'll detect * these as regular int[2] or long[2] values. * * @deprecated DO NOT USE -- it will be removed in the future. * * @throws FitsException if the table could not be copied and threw a {@link nom.tam.util.TableException}, which * is preserved as the cause. * * @see #copy() */ public BinaryTable(ColumnTable tab) throws FitsException { this(); table = new ColumnTable<>(); nRow = tab.getNRows(); columns = new ArrayList<>(); for (int i = 0; i < tab.getNCols(); i++) { int n = tab.getElementSize(i); ColumnDesc c = new ColumnDesc(tab.getElementClass(i), n > 1 ? new int[] {n} : SINGLETON_SHAPE); addFlattenedColumn(tab.getColumn(i), nRow, c, true); } } /** * Creates a binary table from a given FITS header description. The table columns are initialized but no data will * be available, at least initially. Data may be loaded later (e.g. deferred read mode), provided the table is * associated to an input (usually only if this constructor is called from a {@link Fits} object reading an input). * When the table has an input configured via a {@link Fits} object, the table entries may be accessed in-situ in * the file while in deferred read mode, but operations affecting significant portions of the table (e.g. retrieving * all data via {@link #getData()} or accessing entire columns) may load the data in memory. You can also call * {@link #detach()} any time to force loading the data into memory, so that alterations after that will not be * reflected in the original file, at least not unitl {@link #rewrite()} is called explicitly. * * @param header A FITS header describing what the binary table should look like. * * @throws FitsException if the specified header is not usable for a binary table * * @deprecated (for internal use) This constructor should only be called from a {@link Fits} * object reading an input; visibility may be reduced to the package level in the * future. * * @see #isDeferred() */ public BinaryTable(Header header) throws FitsException { String ext = header.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE); if (!ext.equalsIgnoreCase(Standard.XTENSION_BINTABLE) && !ext.equalsIgnoreCase(NonStandard.XTENSION_A3DTABLE)) { throw new FitsException( "Not a binary table header (XTENSION = " + header.getStringValue(Standard.XTENSION) + ")"); } synchronized (this) { nRow = header.getIntValue(Standard.NAXIS2); } long tableSize = nRow * header.getLongValue(Standard.NAXIS1); long paramSizeL = header.getLongValue(Standard.PCOUNT); long heapOffsetL = header.getLongValue(Standard.THEAP, tableSize); // Subtract out the size of the regular table from // the heap offset. long heapSizeL = (tableSize + paramSizeL) - heapOffsetL; if (heapSizeL < 0) { throw new FitsException("Inconsistent THEAP and PCOUNT"); } if (heapSizeL > Integer.MAX_VALUE) { throw new FitsException("Heap size > 2 GB"); } if (heapSizeL == 0L) { // There is no heap. Forget the offset heapAddress = 0; } heapAddress = (int) heapOffsetL; heapFileSize = (int) heapSizeL; int nCol = header.getIntValue(Standard.TFIELDS); synchronized (this) { rowLen = 0; columns = new ArrayList<>(); for (int col = 0; col < nCol; col++) { rowLen += processCol(header, col, rowLen); } HeaderCard card = header.getCard(Standard.NAXIS1); card.setValue(rowLen); } } /** * Creates a binary table from existing table data int row-major format. That is the first array index is the row * index while the second array index is the column index. * * @param rowColTable Row / column array. Scalars elements are wrapped in arrays of 1, s.t. a single * int elements is stored as int[1] at its * [row][col] index. * * @throws FitsException if the argument is not a suitable representation of data in rows. * * @deprecated The constructor is ambiguous, use {@link #fromRowMajor(Object[][])} instead. You can * have a column-major array that has no scalar primitives which would also be an * Object[][] and could be passed erroneously. */ public BinaryTable(Object[][] rowColTable) throws FitsException { this(); for (Object[] row : rowColTable) { addRow(row); } } /** * Creates a binary table from existing table data in row-major format. That is the first array index is the row * index while the second array index is the column index; * * @param table Row / column array. Scalars elements are wrapped in arrays of 1, s.t. a single * int elements is stored as int[1] at its * [row][col] index. * * @return a new binary table with the data. The tables data may be partially independent from the * argument. Modifications to the table data, or that to the argument have undefined * effect on the other object. If it is important to decouple them, you can use a * {@link ArrayFuncs#deepClone(Object)} of your original data as an argument. * * @throws FitsException if the argument is not a suitable representation of FITS data in rows. * * @see #fromColumnMajor(Object[]) * * @since 1.18 */ public static BinaryTable fromRowMajor(Object[][] table) throws FitsException { BinaryTable tab = new BinaryTable(); for (Object[] row : table) { tab.addRow(row); } return tab; } /** * Create a binary table from existing data in column-major format order. * * @param columns array of columns. The data for scalar entries is a primive array. For all else, the * entry is an Object[] array of sorts. * * @throws FitsException if the data for the columns could not be used as coulumns * * @deprecated The constructor is ambiguous, use {@link #fromColumnMajor(Object[])} instead. One could * call this method with any row-major Object[][] table by mistake. * * @see #defragment() */ public BinaryTable(Object[] columns) throws FitsException { this(); for (Object element : columns) { addColumn(element); } } /** * Creates a binary table from existing data in column-major format order. * * @param columns array of columns. The data for scalar entries is a primive array. For all else, the entry * is an Object[] array of sorts. * * @return a new binary table with the data. The tables data may be partially independent from the * argument. Modifications to the table data, or that to the argument have undefined * effect on the other object. If it is important to decouple them, you can use a * {@link ArrayFuncs#deepClone(Object)} of your original data as an argument. * * @throws FitsException if the argument is not a suitable representation of FITS data in rows. * * @see #fromColumnMajor(Object[]) * * @since 1.18 */ public static BinaryTable fromColumnMajor(Object[] columns) throws FitsException { BinaryTable t = new BinaryTable(); for (Object element : columns) { t.addColumn(element); } return t; } @Override protected BinaryTable clone() { try { return (BinaryTable) super.clone(); } catch (CloneNotSupportedException e) { return null; } } /** * Returns an independent copy of the binary table. * * @return a new binary that tnat contains an exact copy of us, but is completely independent. * * @throws FitsException if the table could not be copied * * @since 1.18 */ public synchronized BinaryTable copy() throws FitsException { BinaryTable copy = clone(); synchronized (copy) { if (table != null) { copy.table = table.copy(); } if (heap != null) { copy.heap = heap.copy(); } copy.columns = new ArrayList<>(); for (ColumnDesc c : columns) { c = c.clone(); copy.columns.add(c); } } return copy; } /** * (for internal use) Discards all variable-length arrays from this table, that is all data stored on the * heap, and resets all heap descritors to (0,0). * * @since 1.19.1 */ protected synchronized void discardVLAs() { for (int col = 0; col < columns.size(); col++) { ColumnDesc c = columns.get(col); if (c.isVariableSize()) { for (int row = 0; row < nRow; row++) { table.setElement(row, col, c.hasLongPointers() ? new long[2] : new int[2]); } } } heap = new FitsHeap(0); } /** * Returns the number of bytes per regular table row * * @return the number of bytes in a regular table row. */ final synchronized int getRowBytes() { return rowLen; } /** * @deprecated (for internal use) It may become a private method in the future. * * @param table the table to create the column data. * * @throws FitsException if the data could not be created. */ public static void createColumnDataFor(BinaryTable table) throws FitsException { synchronized (table) { table.createTable(table.nRow); } } /** * @deprecated (for internal use) It may be reduced to private visibility in the future. Parse the * TDIMS value. If the TDIMS value cannot be deciphered a one-d array with the size given in * arrsiz is returned. * * @param tdims The value of the TDIMSn card. * * @return An int array of the desired dimensions. Note that the order of the tdims is the inverse of the * order in the TDIMS key. */ public static int[] parseTDims(String tdims) { if (tdims == null) { return null; } // The TDIMs value should be of the form: "(i,j...)" int start = tdims.indexOf('('); if (start < 0) { return null; } int end = tdims.indexOf(')', start); if (end < 0) { end = tdims.length(); } StringTokenizer st = new StringTokenizer(tdims.substring(start + 1, end), ","); int dim = st.countTokens(); if (dim > 0) { int[] dims = new int[dim]; for (int i = dim; --i >= 0;) { dims[i] = Integer.parseInt(st.nextToken().trim()); } return dims; } return null; } /** *

* Adds a column of complex values stored as the specified decimal type of components in the FITS. While you can * also use {@link #addColumn(Object)} to add complex values, that method will always add them as 64-bit * double-precision values. So, this method is provided to allow users more control over how they want their complex * data be stored. *

*

* The new column will be named as "Column n" (where n is the 1-based index of the column) by default, * which can be changed by {@link ColumnDesc#name(String)} after. *

* * @param o A {@link ComplexValue} or an array (possibly multi-dimensional) thereof. * @param decimalType float.class or double.class (all other values default to * double.class). * * @return the number of column in the table including the new column. * * @throws FitsException if the object contains values other than {@link ComplexValue} types or if the array is not * suitable for storing in the FITS, e.g. because it is multi-dimensional but varying in * shape / size. * * @since 1.18 * * @see #addColumn(Object) */ public int addComplexColumn(Object o, Class decimalType) throws FitsException { int col = columns.size(); int eSize = addColumn(ArrayFuncs.complexToDecimals(o, decimalType)); ColumnDesc c = columns.get(col); c.isComplex = true; c.setLegacyShape(c.fitsShape); return eSize; } /** *

* Adds a column of string values (one per row), optimized for storage size. Unlike {@link #addColumn(Object)}, * which always store strings in fixed format, this method will automatically use variable-length columns for * storing the strings if their lengths vary sufficiently to make that form of storage more efficient, or if the * array contains nulls (which may be defined later). *

*

* The new column will be named as "Column n" (where n is the 1-based index of the column) by default, * which can be changed by {@link ColumnDesc#name(String)} after. *

* * @param o A 1D string array, with 1 string element per table row. The array may contain * null entries, in which case variable-length columns will be used, since * these may be defined later... * * @return the number of column in the table including the new column. * * @throws FitsException if the object contains values other than {@link ComplexValue} types or if the array is not * suitable for storing in the FITS, e.g. because it is multi-dimensional but varying in * shape / size. * * @since 1.18 * * @see #addColumn(Object) */ public int addStringColumn(String[] o) throws FitsException { checkRowCount(o); ColumnDesc c = new ColumnDesc(String.class); // Check if we should be using variable-length strings // (provided its a scalar string column with sufficiently varied strings sizes to make it worth.. int min = FitsUtil.minStringLength(o); int max = FitsUtil.maxStringLength(o); if (max - min > 2 * ElementType.forClass(c.pointerClass()).size()) { c = ColumnDesc.createForVariableSize(String.class); return addVariableSizeColumn(o, c); } c = ColumnDesc.createForStrings(max); return addFlattenedColumn(o, o.length, c, false); } /** *

* Adds a column of bits. This uses much less space than if adding boolean values as logicals (the default behaviot * of {@link #addColumn(Object)}, since logicals take up 1 byte per element, whereas bits are really single bits. *

*

* The new column will be named as "Column n" (where n is the 1-based index of the column) by default, * which can be changed by {@link ColumnDesc#name(String)} after. *

* * @param o An any-dimensional array of boolean values. * * @return the number of column in the table including the new column. * * @throws IllegalArgumentException if the argument is not an array of boolean values. * @throws FitsException if the object is not an array of boolean values. * * @since 1.18 * * @see #addColumn(Object) */ public int addBitsColumn(Object o) throws FitsException { if (ArrayFuncs.getBaseClass(o) != boolean.class) { throw new IllegalArgumentException("Not an array of booleans: " + o.getClass()); } return addColumn(o, false); } /** *

* Adds a new empty column to the table to the specification. This is useful when the user may want ot have more * control on how columns are configured before calling {@link #addRow(Object[])} to start populating. The new * column will be named as "Column n" (where n is the 1-based index of the column) by default, unless * already named otherwise. *

*

* The new column will be named as "Column n" (where n is the 1-based index of the column) by default, * which can be changed by {@link ColumnDesc#name(String)} after. *

* * @param descriptor the column descriptor * * @return the number of table columns after the addition * * @throws IllegalStateException if the table already contains data rows that prevent the addition of empty * comlumns. * * @see #addRow(Object[]) * @see ColumnDesc#name(String) */ public synchronized int addColumn(ColumnDesc descriptor) throws IllegalStateException { if (nRow != 0) { throw new IllegalStateException("Cannot add empty columns to table already containing data rows"); } descriptor.offset = rowLen; rowLen += descriptor.rowLen(); if (descriptor.name() == null) { // Set default column name; descriptor.name(TableHDU.getDefaultColumnName(columns.size())); } columns.add(descriptor); return columns.size(); } /** *

* Adds a new column with the specified data array, with some default mappings. This method will always use * double-precision representation for {@link ComplexValue}-based data, and will represent boolean * based array data as one-byte-per element FITS logical values (for back compatibility). It will also store strings * as fixed sized (sized for the longest string element contained). *

*

* The new column will be named as "Column n" (where n is the 1-based index of the column) by default, * which can be changed by {@link ColumnDesc#name(String)} after. *

*

* If you want other complex-valued representations use {@link #addComplexColumn(Object, Class)} instead, and if you * want to pack boolean-based data more efficiently (using up to 8 times less space), use * {@link #addBitsColumn(Object)} instead, or else convert the column to bits afterwards using * {@link #convertToBits(int)}. And, if you want to allow storing strings more effiently in variable-length columns, * you should use {@link #addStringColumn(String[])} instead. *

*

* As of 1.18, the argument can be a boxed primitive for a coulmn containing a single scalar-valued entry (row). *

* * @see #addVariableSizeColumn(Object) * @see #addComplexColumn(Object, Class) * @see #addBitsColumn(Object) * @see #convertToBits(int) * @see #addStringColumn(String[]) * @see ColumnDesc#name(String) */ @Override public int addColumn(Object o) throws FitsException { return addColumn(o, true); } private synchronized int checkRowCount(Object o) throws FitsException { if (!o.getClass().isArray()) { throw new TableException("Not an array: " + o.getClass().getName()); } int rows = Array.getLength(o); if (columns.size() != 0 && rows != nRow) { throw new TableException("Mismatched number of rows: " + rows + ", expected " + nRow); } return rows; } /** * Like {@link #addColumn(Object)}, but allows specifying whether we use back compatible mode. This mainly just * affects how boolean arrays are stored (as logical bytes in compatibility mode, or as packed bits * otherwise). * * @param Whether to add the column in a back compatibility mode with versions prior to 1.18. If true * boolean arrays will stored as logical bytes, otherwise as packed bits. */ private int addColumn(Object o, boolean compat) throws FitsException { o = ArrayFuncs.objectToArray(o, compat); int rows = checkRowCount(o); ColumnDesc c = new ColumnDesc(ArrayFuncs.getBaseClass(o)); if (ArrayFuncs.getBaseClass(o) == ComplexValue.class) { o = ArrayFuncs.complexToDecimals(o, double.class); c.isComplex = true; } try { int[] dim = ArrayFuncs.checkRegularArray(o, c.isNullAllowed()); if (c.isString()) { c.setStringLength(FitsUtil.maxStringLength(o)); } if (c.isComplex) { // Drop the railing 2 dimension, keep only outer dims... dim = Arrays.copyOf(dim, dim.length - 1); o = ArrayFuncs.flatten(o); } if (dim.length <= 1) { c.setSingleton(); } else { int[] shape = new int[dim.length - 1]; System.arraycopy(dim, 1, shape, 0, shape.length); c.setLegacyShape(shape); o = ArrayFuncs.flatten(o); } } catch (IllegalArgumentException e) { c.setVariableSize(false); return addVariableSizeColumn(o, c); } // getBaseClass() prevents heterogeneous columns, so no need to catch ClassCastException here. return addFlattenedColumn(o, rows, c, compat); } /** *

* Adds a new variable-length data column, populating it with the specified data object. Unlike * {@link #addColumn(Object)} which will use fixed-size data storage provided the data allows it, this method forces * the use of variable-sized storage regardless of the data layout -- for example to accommodate addiing rows / * elements of different sized at a later time. *

*

* The new column will be named as "Column n" (where n is the 1-based index of the column) by default, * which can be changed by {@link ColumnDesc#name(String)} after. *

* * @param o An array containing one entry per row. Multi-dimensional entries will be flattened to 1D * for storage on the heap. * * @return the number of table columns after the addition. * * @throws FitsException if the column could not be created as requested. * * @see #addColumn(Object) * @see #addColumn(ColumnDesc) * @see ColumnDesc#createForVariableSize(Class) * @see ColumnDesc#isVariableSize() * * @since 1.18 */ public int addVariableSizeColumn(Object o) throws FitsException { Class base = ArrayFuncs.getBaseClass(o); ColumnDesc c = ColumnDesc.createForVariableSize(base); return addVariableSizeColumn(o, c); } /** * Adds a new column with data directly, without performing any checks on the data. This should only be use * internally, after ansuring the data integrity and suitability for this table. * * @param o the column data, whose integrity was verified previously * @param rows the number of rows the data contains (in flattened form) * @param c the new column's descriptor * * @return the number of table columns after the addition * * @throws FitsException if the data is not the right type or format for internal storage. */ private synchronized int addDirectColumn(Object o, int rows, ColumnDesc c) throws FitsException { c.offset = rowLen; rowLen += c.rowLen(); // Load any deferred data (we will not be able to do that once we alter the column structure) ensureData(); // Set the default column name c.name(TableHDU.getDefaultColumnName(columns.size())); table.addColumn(o, c.getTableBaseCount()); columns.add(c); if (nRow == 0) { // Set the table row count to match first colum nRow = rows; } return columns.size(); } private int addVariableSizeColumn(Object o, ColumnDesc c) throws FitsException { checkRowCount(o); Object[] array = (Object[]) o; o = Array.newInstance(c.pointerClass(), array.length * 2); for (int i = 0; i < array.length; i++) { boolean multi = c.isComplex() ? array[i] instanceof Object[][] : array[i] instanceof Object[]; if (multi) { boolean canBeComplex = false; if (c.getFitsBase() == float.class || c.getFitsBase() == double.class) { int[] dim = ArrayFuncs.getDimensions(array[i]); if (dim[dim.length - 1] == 2) { canBeComplex = true; } } if (!canBeComplex && !c.warnedFlatten) { LOG.warning("Table entries of " + array[i].getClass() + " will be stored as 1D arrays in variable-length columns. " + "Array shape(s) and intermittent null subarrays (if any) will be lost."); c.warnedFlatten = true; } } Object p = putOnHeap(c, array[i], null); System.arraycopy(p, 0, o, 2 * i, 2); } return addDirectColumn(o, array.length, c); } /** * Add a column where the data is already flattened. * * @param o The new column data. This should be a one-dimensional primitive array. * @param dims The dimensions of an element in the column, or null for singleton (scalar) columns * * @return the new column size * * @throws FitsException if the array could not be flattened * * @deprecated (for internal use) No longer used, will be removed in the future */ public int addFlattenedColumn(Object o, int... dims) throws FitsException { ColumnDesc c = new ColumnDesc(ArrayFuncs.getBaseClass(o)); try { ArrayFuncs.checkRegularArray(o, c.isNullAllowed()); } catch (IllegalArgumentException e) { throw new FitsException("Irregular array: " + o.getClass() + ": " + e.getMessage(), e); } if (c.isString()) { c.setStringLength(FitsUtil.maxStringLength(o)); } int n = 1; c.setLegacyShape(dims); for (int dim : dims) { n *= dim; } int rows = Array.getLength(o) / n; return addFlattenedColumn(o, rows, c, true); } /** * Checks that a flattened column has a compatible size for storing in a fixed-width column. It will also log a * warning if the storage size of the object is zero. * * @param c the column descriptor * @param o the column data * * @throws FitsException if the data is not the right size for the column */ private synchronized void checkFlattenedColumnSize(ColumnDesc c, Object o) throws FitsException { if (c.getTableBaseCount() == 0) { LOG.warning("Elements of column + " + columns.size() + " have zero storage size."); } else if (columns.size() > 0) { // Check that the number of rows is consistent. int l = Array.getLength(o); if (nRow > 0 && l != nRow * c.getTableBaseCount()) { throw new TableException("Mismatched element count " + l + ", expected " + (nRow * c.getTableBaseCount())); } } } /** * This function is needed since we had made addFlattenedColumn public so in principle a user might have called it * directly. * * @param o The new column data. This should be a one-dimensional primitive array. * @param c The column description * * @return the new column size * * @throws FitsException if the data type, format, or element count is inconsistent with this table. */ private int addFlattenedColumn(Object o, int rows, ColumnDesc c, boolean compat) throws FitsException { // For back compatibility this method will add boolean values as logicals always... if (compat) { c.isBits = false; } if (c.isBits) { // Special handling for bits, which have to be segmented into bytes... boolean[] bits = (boolean[]) o; o = FitsUtil.bitsToBytes(bits, bits.length / rows); } else { o = javaToFits1D(c, o); } checkFlattenedColumnSize(c, o); return addDirectColumn(o, rows, c); } /** *

* Adds a row to the table. If this is the first row in a new table, fixed-length columns will be created from the * data type automatically. If you want more control over the column formats, you may want to specify columns * beforehand such as: *

* *
     *   BinaryTable table = new BinaryTable();
     *   
     *   // A column containing 64-bit floating point scalar values, 1 per row...
     *   table.addColumn(ColumnDesc.createForScalars(double.class));
     *   
     *   // A column containing 5x4 arrays of single-precision complex values...
     *   table.addColumn(ColumnDesc.createForArrays(ComplexValue.Float.class, 5, 4)
     *  
     *   // A column containing Strings of variable length using 32-bit heap pointers...
     *   table.addColumn(ColumnDesc.creatForVariableStrings(false);
     * 
*

* For scalar columns of primitive types, the argument may be the corresponding java boxed type (new style), or a * primitive array of 1 (old style). Thus, you can write either: *

* *
     * table.addRow(1, 3.14159265);
     * 
*

* or, *

* *
     *   table.addRow(new Object[] { new int[] {1}, new double[] {3.14159265} };
     * 
* * @see #addColumn(ColumnDesc) */ @Override public synchronized int addRow(Object[] o) throws FitsException { if (columns.isEmpty()) { for (Object element : o) { if (element == null) { throw new TableException("Prototype row may not contain null"); } Class cl = element.getClass(); if (cl.isArray()) { if (cl.getComponentType().isPrimitive() && Array.getLength(element) == 1) { // Primitives of 1 (e.g. short[1]) are wrapped and should be added as is. addColumn(element); } else { // Wrap into array of 1, as leading dimension becomes the number of rows, which must be 1... Object wrapped = Array.newInstance(element.getClass(), 1); Array.set(wrapped, 0, element); addColumn(wrapped); } } else { addColumn(ArrayFuncs.objectToArray(element, true)); } } return 1; } if (o.length != columns.size()) { throw new TableException("Mismatched row size: " + o.length + ", expected " + columns.size()); } ensureData(); Object[] flatRow = new Object[getNCols()]; for (int i = 0; i < flatRow.length; i++) { ColumnDesc c = columns.get(i); if (c.isVariableSize()) { flatRow[i] = putOnHeap(c, o[i], null); } else { flatRow[i] = javaToFits1D(c, ArrayFuncs.flatten(o[i])); int nexp = c.getElementCount(); if (c.stringLength > 0) { nexp *= c.stringLength; } if (Array.getLength(flatRow[i]) != nexp) { throw new IllegalArgumentException("Mismatched element count for column " + i + ": got " + Array.getLength(flatRow[i]) + ", expected " + nexp); } } } table.addRow(flatRow); nRow++; return nRow; } @Override public synchronized void deleteColumns(int start, int len) throws FitsException { ensureData(); table.deleteColumns(start, len); ArrayList remain = new ArrayList<>(columns.size() - len); rowLen = 0; for (int i = 0; i < columns.size(); i++) { if (i < start || i >= start + len) { ColumnDesc c = columns.get(i); c.offset = rowLen; rowLen += c.rowLen(); remain.add(c); } } columns = remain; } @Override public synchronized void deleteRows(int row, int len) throws FitsException { ensureData(); table.deleteRows(row, len); nRow -= len; } /** * Returns the Java type of elements returned or expected by the older srray-based access methods. It can be * confusing, because: *
    *
  • Columns with variable sized entries report int.class or long.class regardless of * data type.
  • *
  • Regular logical and bit columns bith report boolean.class.
  • *
  • Regular complex valued columns report float.class or double.class.
  • *
* * @return the types in the table, not the underlying types (e.g., for varying length arrays or booleans). * * @deprecated (for internal use) Ambiguous, use {@link ColumnDesc#getElementClass()} instead. Will remove in * the future. */ public synchronized Class[] getBases() { return table.getBases(); } /** *

* Returns the data for a particular column in as an array of elements. See {@link #addColumn(Object)} for more * information about the format of data elements in general. *

* * @param col The zero-based column index. * * @return an array of primitives (for scalar columns), or else an Object[] array, or * possibly null * * @throws FitsException if the table could not be accessed * * @see #setColumn(int, Object) * @see #getElement(int, int) * @see #getNCols() */ @Override public synchronized Object getColumn(int col) throws FitsException { ColumnDesc c = columns.get(col); if (!c.isVariableSize() && c.fitsDimension() == 0 && !c.isComplex()) { return getFlattenedColumn(col); } ensureData(); Object[] data = null; for (int i = 0; i < nRow; i++) { Object e = getElement(i, col); if (data == null) { data = (Object[]) Array.newInstance(e.getClass(), nRow); } data[i] = e; } return data; } /** * Returns the Java index of the first column by the specified name. * * @param name the name of the column (case sensitive). * * @return The column index, or else -1 if this table does not contain a column by the specified name. * * @see #getDescriptor(String) * @see ColumnDesc#name(String) * * @since 1.20 * * @author Attila Kovacs */ public int indexOf(String name) { for (int col = 0; col < columns.size(); col++) { if (name.equals(getDescriptor(col).name())) { return col; } } return -1; } @Override protected synchronized ColumnTable getCurrentData() { return table; } @Override public ColumnTable getData() throws FitsException { return (ColumnTable) super.getData(); } /** * Returns the dimensions of elements in each column. * * @return an array of arrays with the dimensions of each column's data. * * @see ColumnDesc#getDimens() * * @deprecated (for internal use) Use {@link ColumnDesc#getEntryShape()} to access the shape of Java elements * individually for columns instead. Not useful to users since it returns the dimensions of the * primitive storage types, which is not always the dimension of elements on the Java side (notably * for string entries). */ public int[][] getDimens() { int[][] dimens = new int[columns.size()][]; for (int i = 0; i < dimens.length; i++) { dimens[i] = columns.get(i).getDimens(); } return dimens; } /** * @deprecated (for internal use) It may be private in the future. * * @return An array with flattened data, in which each column's data is represented by a 1D array * * @throws FitsException if the reading of the data failed. */ public Object[] getFlatColumns() throws FitsException { ensureData(); return table.getColumns(); } /** * @deprecated (for internal use) It may be reduced to private visibility in the future. * * @return column in flattened format. This is sometimes useful for fixed-sized columns. * Variable-sized columns will still return an Object[] array in which * each entry is the variable-length data for a row. * * @param col the column to flatten * * @throws FitsException if the column could not be flattened */ public synchronized Object getFlattenedColumn(int col) throws FitsException { if (!validColumn(col)) { throw new TableException("Invalid column index " + col + " in table of " + getNCols() + " columns"); } ColumnDesc c = columns.get(col); if (c.isVariableSize()) { throw new TableException("Cannot flatten variable-sized column data"); } ensureData(); if (c.isBits()) { boolean[] bits = new boolean[nRow * c.fitsCount]; for (int i = 0; i < nRow; i++) { boolean[] seg = (boolean[]) fitsToJava1D(c, table.getElement(i, col), c.fitsCount, false); System.arraycopy(seg, 0, bits, i * c.fitsCount, c.fitsCount); } return bits; } return fitsToJava1D(c, table.getColumn(col), 0, false); } /** *

* Reserves space for future addition of rows at the end of the regular table. In effect, this pushes the heap to * start at an offset value, leaving a gap between the main table and the heap in the FITS file. If your table * contains variable-length data columns, you may also want to reserve extra heap space for these via * {@link #reserveHeapSpace(int)}. *

*

* Note, that (C)FITSIO, as of version 4.4.0, has no proper support for offset heaps, and so you may want to be * careful using this function as the resulting FITS files, while standard, may not be readable by other tools due * to their own lack of support. Note, however, that you may also use this function to undo an offset heap with an * argument <=0; *

* * @param rows The number of future rows fow which space should be reserved (relative to the current table size) * for future additions, or <=0 to ensure that the heap always follows immediately after the * main table, e.g. for better (C)FITSIO interoperability. * * @see #reserveHeapSpace(int) * * @since 1.19.1 * * @author Attila Kovacs */ public synchronized void reserveRowSpace(int rows) { heapAddress = rows > 0 ? getRegularTableSize() + (long) rows * getRowBytes() : 0; } /** * Reserves space in the file at the end of the heap for future heap growth (e.g. different/longer or new VLA * entries). You may generally want to call this along with {@link #reserveRowSpace(int)} if yuor table contains * variable-length columns, to ensure storage for future data in these. You may call with <=0 to discards any * previously reserved space. * * @param bytes The number of bytes of unused space to reserve at the end of the heap, e.g. for future * modifications or additions, when writing the data to file. * * @see #reserveRowSpace(int) * * @since 1.19.1 * * @author Attila Kovacs */ public synchronized void reserveHeapSpace(int bytes) { heapReserve = Math.max(0, bytes); } /** * Returns the address of the heap from the star of the HDU in the file. * * @return (bytes) the start of the heap area from the beginning of the HDU. */ final synchronized long getHeapAddress() { long tableSize = getRegularTableSize(); return heapAddress > tableSize ? heapAddress : tableSize; } /** * Returns the offset from the end of the main table * * @return the offset to the heap */ final long getHeapOffset() { return getHeapAddress() - getRegularTableSize(); } /** * It returns the heap size for storing in the FITS, which is the larger of the actual space occupied by the current * heap, or the original heap size based on the header when the HDU was read from an input. In the former case it * will also include heap space reserved for future additions. * * @return (byte) the size of the heap in the FITS file. * * @see #compact() * @see #reserveHeapSpace(int) */ private synchronized int getHeapSize() { if (heap != null && heap.size() + heapReserve > heapFileSize) { return heap.size() + heapReserve; } return heapFileSize; } /** * @return the size of the heap -- including the offset from the end of the table data, and reserved space after. */ synchronized long getParameterSize() { return getHeapOffset() + getHeapSize(); } /** * Returns an empty row for the table. Such model rows are useful when low-level reading binary tables from an input * row-by-row. You can simply all {@link nom.tam.util.ArrayDataInput#readArrayFully(Object)} to populate it with * data from a stream. You may also use model rows to add additional rows to an existing table. * * @return a row that may be used for direct i/o to the table. * * @deprecated (for internal use) Use {@link #getElement(int, int)} instead for low-level reading of tables * in deferred mode. Not recommended for uses because it requires a deep understanding of how data * (especially varialbe length columns) are represented in the FITS. Will reduce visibility to * private in the future. */ public Object[] getModelRow() { Object[] modelRow = new Object[columns.size()]; for (int i = 0; i < modelRow.length; i++) { ColumnDesc c = columns.get(i); if (c.fitsDimension() < 2) { modelRow[i] = Array.newInstance(c.getTableBase(), c.getTableBaseCount()); } else { modelRow[i] = Array.newInstance(c.getTableBase(), c.fitsShape); } } return modelRow; } @Override public int getNCols() { return columns.size(); } @Override public synchronized int getNRows() { return nRow; } /** * Reads a regular table element in the main table from the input. This method should never be called unless we have * a random-accessible input associated, which is a requirement for deferred read mode. * * @param o The array element to populate * @param c the column descriptor * @param row the zero-based row index of the element * * @throws IOException If there was an I/O error accessing the input * @throws FitsException If there was some other error */ private synchronized void readTableElement(Object o, ColumnDesc c, int row) throws IOException, FitsException { @SuppressWarnings("resource") RandomAccess in = getRandomAccessInput(); synchronized (this) { in.position(getFileOffset() + row * (long) rowLen + c.offset); } if (c.isLogical()) { in.readArrayFully(o); } else { in.readImage(o); } } /** * Returns an unprocessed element from the table as a 1D array of the elements that are stored in the regular table * data, whithout reslving heap references. That is this call will return flattened versions of multidimensional * arrays, and will return only the heap locator (offset and size) for variable-sized columns. * * @return a particular element from the table but do no processing of this element (e.g., * dimension conversion or extraction of variable length array elements/) * * @param row The row of the element. * @param col The column of the element. * * @deprecated (for internal use) Will reduce visibility in the future. * * @throws FitsException if the operation failed */ public synchronized Object getRawElement(int row, int col) throws FitsException { if (!validRow(row) || !validColumn(col)) { throw new TableException("No such element (" + row + "," + col + ")"); } if (table == null) { try { ColumnDesc c = columns.get(col); Object e = c.newInstance(1); readTableElement(e, c, row); return e; } catch (IOException e) { throw new FitsException("Error reading from input: " + e.getMessage(), e); } } ensureData(); return table.getElement(row, col); } /** * Returns a table element as a Java array. Consider using the more Java-friendly {@link #get(int, int)} or one of * the scalar access methods with implicit type conversion support. * * @see #get(int, int) * @see #getLogical(int, int) * @see #getNumber(int, int) * @see #getLong(int, int) * @see #getDouble(int, int) * @see #getString(int, int) */ @Override public Object getElement(int row, int col) throws FitsException { return getElement(row, col, false); } /** * Returns a a table entry, with control over how FITS logical values are to be handled. * * @param row zero-based row index * @param col zero-based column index * @param isEnhanced Whether logicals should be returned as {@link Boolean} (rather than boolean) * and complex values as {@link ComplexValue} (rather than float[2] or * double[2]), or arrays thereof. Methods prior to 1.18 should set this to * false for back compatible behavior. * * @return The entry as a primitive array, or {@link String}, {@link Boolean} or {@link ComplexValue}, * or arrays thereof. * * @throws FitsException If the requested element could not be accessed. */ private Object getElement(int row, int col, boolean isEnhanced) throws FitsException { if (!validRow(row) || !validColumn(col)) { throw new TableException("No such element (" + row + "," + col + ")"); } ColumnDesc c = columns.get(col); Object o = getRawElement(row, col); if (c.isVariableSize()) { return getFromHeap(c, o, isEnhanced); } o = fitsToJava1D(c, o, c.isBits() ? c.fitsCount : 0, isEnhanced); if (c.legacyShape.length > 1) { return ArrayFuncs.curl(o, c.legacyShape); } return o; } /** * Returns a table element as an array of the FITS storage type. Similar to the original * {@link #getElement(int, int)}, except that FITS logicals are returned as arrays of Boolean (rather * than boolean), bits are returned as arrays of boolean, and complex values are returned * as arrays of {@link ComplexValue} rather than arrays of double[2] or float[2]. * Singleton (scalar) table elements are not boxed to an enclosing Java type (unlike {@link #get(int, int)}), an * instead returned as arrays of just one element. For example, a single logical as a Boolean[1], a * single float as a float[1] or a single double-precision complex value as * ComplexValue[1]. * * @param row zero-based row index * @param col zero-based column index * * @return The table entry as an array of the stored Java type, without applying any type or quantization * conversions. * * @see #getArrayElementAs(int, int, Class) * @see #get(int, int) * * @since 1.20 */ public Object getArrayElement(int row, int col) { return getElement(row, col, true); } /** *

* Returns a numerical table element as an array of a specific underlying other numerical type. Similar * {@link #getArrayElement(int, int)} except that table entries are converted to the specified array type before * returning. If an integer-decimal conversion is involved, it will be performed through the column's quantizer (if * any) or else via a simple rounding as necessary. *

*

* For example, if you have an short-type column, and you want is an array of double * values that are represented by the 16-bit integers, then the conversion will use the column's quantizer scaling * and offset before returning the result either as an array of doubles, and the designated short * blanking values will be converted to NaNs. *

* * @param row zero-based row index * @param col zero-based column index * @param asType The desired underlying type, a primitive class or a {@link ComplexValue} type * for appropriate numerical arrays (with a trailing Java dimension of 2 for * the real/imaginary pairs). * * @return An array of the desired type (e.g. double[][] if * asType is double.class and the column contains 2D * arrays of some numerical type). * * @throws IllegalArgumentException if the numerical conversion is not possible for the given column type or if the * type argument is not a supported numerical primitive or {@link ComplexValue} * type. * * @see #getArrayElement(int, int) * * @since 1.20 */ public Object getArrayElementAs(int row, int col, Class asType) throws IllegalArgumentException { ColumnDesc c = getDescriptor(col); Object e = getElement(row, col, true); return asType.isAssignableFrom(c.getFitsBase()) ? e : ArrayFuncs.convertArray(e, asType, c.getQuantizer()); } /** *

* Returns a table element using the usual Java boxing for primitive scalar (singleton) entries, or packaging * complex values as {@link ComplexValue}, or as appropriate primitive or object arrays. FITS string columns return * {@link String} values. Logical (boolean columns will return a {@link Boolean}, which may be * null if undefined (as per the FITS standard). Multibit FITS bits colums return arrays of * boolean. *

*

* As opposed to {@link #getElement(int, int)} scalar (singleton) values are not wrapped into primitive arrays, but * return either a singular object, such as a ({@link String}, or a {@link ComplexValue}, or a boxed Java primitive. * Thus, columns containing single short entries will return the selected element as a {@link Short}, * or columns containing single double values will return the element as a {@link Double} and so on. *

*

* Array columns will return the expected arrays of primitive values, or arrays of one of the mentioned types. Note * however, that logical arrays are returned as arrays of {@link Boolean}, e.g. Boolean[][], not * boolean[][]. This is because FITS allows null values for logicals beyond * true and false, which is reproduced by the boxed type, but not by the primitive type. FITS * columns of bits (generally preferrably to logicals if support for null values is not required) will * return arrays of boolean. *

*

* Columns containing multidimensional arrays, will return the expected multidimensional array of the above * mentioned types for the FITS storage type. You can then convert numerical arrays to other types as required for * your application via {@link ArrayFuncs#convertArray(Object, Class, Quantizer)}, including any appropriate * quantization for the colummn (see {@link ColumnDesc#getQuantizer()}). *

* * @param row the zero-based row index * @param col the zero-based column index * * @return the element, either as a Java boxed type (for scalar entries), a singular Java Object, or * as a (possibly multi-dimensional) array of {@link String}, {@link Boolean}, * {@link ComplexValue}, or primitives. * * @throws FitsException if the element could not be obtained * * @see #getNumber(int, int) * @see #getLogical(int, int) * @see #getString(int, int) * @see #getArrayElementAs(int, int, Class) * @see #set(int, int, Object) * * @since 1.18 */ public Object get(int row, int col) throws FitsException { ColumnDesc c = columns.get(col); Object e = getElement(row, col, true); return (c.isSingleton() && e.getClass().isArray()) ? Array.get(e, 0) : e; } /** * Returns the numerical value, if possible, for scalar elements. Scalar numerical columns return the boxed type of * their primitive type. Thus, a column of long values will return {@link Long}, whereas a column of * float values will return a {@link Float}. Logical columns will return 1 if true or 0 if * false, or null if undefined. Array columns and other column types will throw an * exception. * * @param row the zero-based row index * @param col the zero-based column index * * @return the number value of the specified scalar entry * * @throws FitsException if the element could not be obtained * @throws ClassCastException if the specified column in not a numerical scalar type. * @throws NumberFormatException if the it's a string column but the entry does not seem to be a number * * @see #getDouble(int, int) * @see #getLong(int, int) * @see #get(int, int) * * @since 1.18 */ public final Number getNumber(int row, int col) throws FitsException, ClassCastException, NumberFormatException { Object o = get(row, col); if (o instanceof String) { try { return Long.parseLong((String) o); } catch (NumberFormatException e) { return Double.parseDouble((String) o); } } if (o instanceof Boolean) { return ((Boolean) o) ? 1 : 0; } return (Number) o; } /** *

* Returns the decimal value, if possible, of a scalar table entry. See {@link #getNumber(int, int)} for more * information on the conversion process. *

*

* Since version 1.20, if the column has a quantizer and stores integer elements, the conversion to double-precision * will account for the quantization of the column, if any, and will return NaN if the stored integer is the * designated blanking value (if any). To bypass quantization, you can use {@link #getNumber(int, int)} instead * followed by {@link Number#doubleValue()} to to get the stored integer values as a double. *

* * @param row the zero-based row index * @param col the zero-based column index * * @return the number value of the specified scalar entry * * @throws FitsException if the element could not be obtained * @throws ClassCastException if the specified column in not a numerical scalar type. * * @see #getNumber(int, int) * @see #getLong(int, int) * @see #get(int, int) * @see ColumnDesc#getQuantizer() * * @since 1.18 */ public final double getDouble(int row, int col) throws FitsException, ClassCastException { Number n = getNumber(row, col); if (!(n instanceof Float || n instanceof Double)) { Quantizer q = getDescriptor(col).getQuantizer(); if (q != null) { return q.toDouble(n.longValue()); } } return n == null ? Double.NaN : n.doubleValue(); } /** *

* Returns a 64-bit integer value, if possible, of a scalar table entry. Boolean columns will return 1 if * true or 0 if false, or throw a {@link NullPointerException} if undefined. See * {@link #getNumber(int, int)} for more information on the conversion process of the stored data element. *

*

* Additionally, since version 1.20, if the column has a quantizer and stores floating-point elements, the * conversion to integer will include the quantization, and NaN values will be converted to the designated integer * blanking values. To bypass quantization, you can use {@link #getNumber(int, int)} instead followed by * {@link Number#longValue()} to to get the stored floating point values rounded directly to a long. *

* * @param row the zero-based row index * @param col the zero-based column index * * @return the 64-bit integer number value of the specified scalar table entry. * * @throws FitsException if the element could not be obtained * @throws ClassCastException if the specified column in not a numerical scalar type. * @throws IllegalStateException if the column contains a undefined (blanking value), such as a {@link Double#NaN} * when no quantizer is set for the column, or a {@link Boolean} null * value. * * @see #getNumber(int, int) * @see #getDouble(int, int) * @see #get(int, int) * * @since 1.18 */ public final long getLong(int row, int col) throws FitsException, ClassCastException, IllegalStateException { Number n = getNumber(row, col); if (n instanceof Float || n instanceof Double) { Quantizer q = getDescriptor(col).getQuantizer(); if (q != null) { return q.toLong(n.doubleValue()); } } if (Double.isNaN(n.doubleValue())) { throw new IllegalStateException("Cannot convert NaN to long without Quantizer"); } return n.longValue(); } /** * Returns the boolean value, if possible, for scalar elements. It will will returntrue, or * false, or null if undefined. Numerical columns will return null if the * corresponding decimal value is NaN, or false if the value is 0, or else true for all * non-zero values (just like in C). * * @param row the zero-based row index * @param col the zero-based column index * * @return the boolean value of the specified scalar entry, or null if undefined. * * @throws ClassCastException if the specified column in not a scalar boolean type. * @throws FitsException if the element could not be obtained * * @see #get(int, int) * * @since 1.18 */ @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null has specific meaning here") public final Boolean getLogical(int row, int col) throws FitsException, ClassCastException { Object o = get(row, col); if (o == null) { return null; } if (o instanceof Number) { Number n = (Number) o; if (Double.isNaN(n.doubleValue())) { return null; } return n.longValue() != 0; } if (o instanceof Character) { char c = (Character) o; if (c == 'T' || c == 't' || c == '1') { return true; } if (c == 'F' || c == 'f' || c == '0') { return false; } return null; } if (o instanceof String) { return FitsUtil.parseLogical((String) o); } return (Boolean) o; } /** * Returns the string value, if possible, for scalar elements. All scalar columns will return the string * representation of their values, while byte[] and char[] are converted to appropriate * strings. * * @param row the zero-based row index * @param col the zero-based column index * * @return the string representatiof the specified table entry * * @throws ClassCastException if the specified column contains array elements other than byte[] or * char[] * @throws FitsException if the element could not be obtained * * @see #get(int, int) * * @since 1.18 */ public final String getString(int row, int col) throws FitsException, ClassCastException { ColumnDesc c = columns.get(col); Object value = get(row, col); if (value == null) { return "null"; } if (!value.getClass().isArray()) { return value.toString(); } if (c.fitsDimension() > 1) { throw new ClassCastException("Cannot convert multi-dimensional array element to String"); } if (value instanceof char[]) { return String.valueOf((char[]) value).trim(); } if (value instanceof byte[]) { return AsciiFuncs.asciiString((byte[]) value).trim(); } throw new ClassCastException("Cannot convert " + value.getClass().getName() + " to String."); } @Override public Object[] getRow(int row) throws FitsException { if (!validRow(row)) { throw new TableException("Invalid row index " + row + " in table of " + getNRows() + " rows"); } Object[] data = new Object[columns.size()]; for (int col = 0; col < data.length; col++) { data[col] = getElement(row, col); } return data; } /** * Returns the flattened (1D) size of elements in each column of this table. As of 1.18, this method returns a copy * ot the array used internally, which is safe to modify. * * @return an array with the byte sizes of each column * * @deprecated (for internal use) Use {@link ColumnDesc#getElementCount()} instead. This one returns the * number of elements in the FITS representation, not in the java representation. For example, for * {@link String} entries, this returns the number of bytes stored, not the number of strings. * Similarly, for complex values it returns the number of components not the number of values. */ public int[] getSizes() { int[] sizes = new int[columns.size()]; for (int i = 0; i < sizes.length; i++) { sizes[i] = columns.get(i).getTableBaseCount(); } return sizes; } /** * Returns the size of the regular table data, before the heap area. * * @return the size of the regular table in bytes */ private synchronized long getRegularTableSize() { return (long) nRow * rowLen; } @Override protected long getTrueSize() { return getRegularTableSize() + getParameterSize(); } /** * Get the characters describing the base classes of the columns. As of 1.18, this method returns a copy ot the * array used internally, which is safe to modify. * * @return An array of type characters (Java array types), one for each column. * * @deprecated (for internal use) Use {@link ColumnDesc#getElementClass()} instead. Not very useful to users * since this returns the FITS primitive storage type for the data column. */ public char[] getTypes() { char[] types = new char[columns.size()]; for (int i = 0; i < columns.size(); i++) { types[i] = ElementType.forClass(columns.get(i).getTableBase()).type(); } return types; } @Override public synchronized void setColumn(int col, Object o) throws FitsException { ColumnDesc c = columns.get(col); if (c.isVariableSize()) { Object[] array = (Object[]) o; for (int i = 0; i < nRow; i++) { Object p = putOnHeap(c, ArrayFuncs.flatten(array[i]), getRawElement(i, col)); setTableElement(i, col, p); } } else { setFlattenedColumn(col, o); } } /** * Writes an element directly into the random accessible FITS file. Note, this call will not modify the table in * memory (if loaded). This method should never be called unless we have a valid encoder object that can handle the * writing, which is a requirement for deferred read mode. * * @param row the zero-based row index * @param col the zero-based column index * @param array an array object containing primitive types, in FITS storage format. It may be * multi-dimensional. * * @throws IOException the there was an error writing to the FITS output * * @see #setTableElement(int, int, Object) */ @SuppressWarnings("resource") private void writeTableElement(int row, int col, Object array) throws IOException { synchronized (this) { ColumnDesc c = columns.get(col); getRandomAccessInput().position(getFileOffset() + row * (long) rowLen + c.offset); } encoder.writeArray(array); } /** * Sets a table element to an array in the FITS storage format. If the data is in deferred mode it will write the * table entry directly into the file. Otherwise it will update the table entry in memory. For variable sized * column, the heap will always be updated in memory, so you may want to call {@link #rewrite()} when done updating * all entries. * * @param row the zero-based row index * @param col the zero-based column index * @param o an array object containing primitive types, in FITS storage format. It may be * multi-dimensional. * * @throws FitsException if the array is invalid for the given column, or if the table could not be accessed in the * file / input. * * @see #setTableElement(int, int, Object) * @see #getRawElement(int, int) */ private synchronized void setTableElement(int row, int col, Object o) throws FitsException { if (table == null) { try { writeTableElement(row, col, o); } catch (IOException e) { throw new FitsException(e.getMessage(), e); } } else { ensureData(); table.setElement(row, col, o); } } /** * Consider using the more Java-friendly {@link #set(int, int, Object)} with implicit scalar type conversions. * * @see #set(int, int, Object) */ @Override public void setElement(int row, int col, Object o) throws FitsException { ColumnDesc c = columns.get(col); o = c.isVariableSize() ? putOnHeap(c, o, getRawElement(row, col)) : javaToFits1D(c, ArrayFuncs.flatten(o)); setTableElement(row, col, o); } /** *

* The Swiss-army knife of setting table entries, including Java boxing, and with some support for automatic type * conversions. The argument may be one of the following type: *

*
    *
  • Scalar values -- any Java primitive with its boxed type, such as a {@link Double}, or a * {@link Character}.
  • *
  • A single {@link String} or {@link ComplexValue} object. *
  • An array (including multidimensional) of primitive types, or that of {@link Boolean}, {@link ComplexValue}, * or {@link String}.
  • *
*

* For array-type columns the argument needs to match the column type exactly. However, you may call * {@link ArrayFuncs#convertArray(Object, Class, Quantizer)} prior to setting values to convert arrays to the * desired numerical types, including the quantization that is appropriate for the column (see * {@link ColumnDesc#getQuantizer()}). *

*

* For scalar (single element) columns, automatic type conversions may apply, to make setting scalar columns more * flexible: *

*
    *
  • Any numerical column can take any {@link Number} value. The conversion is as if an explicit Java cast were * applied. For example, if setting a double value for a column of single short values it * as if a (short) cast were applied to the value.
  • *
  • Numerical colums can also take {@link Boolean} values which set the entry to 1, or 0, or to * {@link Double#isNaN()} (or the equivalent integer minimum value) if the argument is null. Numerical * columns can also set {@link String} values, by parsing the string according to the numerical type of the * column.
  • *
  • Logical columns can set {@link Boolean} values, including nullvalues, but also any * {@link Number} type. In case of numbers, zero values map to false while definite non-zero values map * to true. {@link Double#isNaN()} maps to a null (or undefined) entry. Loginal columns * can be also set to the {@link String} values of 'true' or 'false', or to a {@link Character} of 'T'/'F' (or * equivalently '1'/'0') and 0 (undefined)
  • *
  • Singular string columns can be set to any scalar type owing to Java's {@link #toString()} method performing * the conversion, as long as the string representation fits into the size constraints (if any) for the string * column.
  • *
*

* Additionally, scalar columns can take single-element array arguments, just like * {@link #setElement(int, int, Object)}. *

* * @param row the zero-based row index * @param col the zero-based column index * @param o the new value to set. For array columns this must match the Java array type * exactly, but for scalar columns additional flexibility is provided for fuzzy * type matching (see description above). * * @throws FitsException if the column could not be set * @throws IllegalArgumentException if the argument cannot be converted to a value for the specified column type. * * @since 1.18 * * @see #get(int, int) */ public void set(int row, int col, Object o) throws FitsException, IllegalArgumentException { ColumnDesc c = columns.get(col); if (o == null) { // Only logicals and strings support 'null' values if (!c.isSingleton()) { throw new TableException("No null values allowed for column of " + c.getLegacyBase() + " arrays."); } else if (c.isString()) { setElement(row, col, ""); } else { setLogical(row, col, null); } } else if (o.getClass().isArray()) { Class eType = ArrayFuncs.getBaseClass(o); if (!c.getFitsBase().isAssignableFrom(eType) && c.isNumeric()) { o = ArrayFuncs.convertArray(o, c.getFitsBase(), c.getQuantizer()); } setElement(row, col, o); } else if (o instanceof String) { setString(row, col, (String) o); } else if (!c.isSingleton()) { throw new TableException("Cannot set scalar values in non-scalar columns"); } else if (c.isString()) { setElement(row, col, o.toString()); } else if (o instanceof Boolean) { setLogical(row, col, (Boolean) o); } else if (o instanceof Character) { setCharacter(row, col, (Character) o); } else if (o instanceof Number) { setNumber(row, col, (Number) o); } else if (o instanceof ComplexValue) { setElement(row, col, o); } else { throw new IllegalArgumentException("Unsupported scalar type: " + o.getClass()); } } /** * Sets a scalar table entry to the specified numerical value. * * @param row the zero-based row index * @param col the zero-based column index * @param value the new number value * * @throws ClassCastException if the specified column in not a numerical scalar type. * @throws FitsException if the table element could not be altered * * @see #getNumber(int, int) * @see #set(int, int, Object) * * @since 1.18 */ private void setNumber(int row, int col, Number value) throws FitsException, ClassCastException { ColumnDesc c = columns.get(col); // Already checked before calling... // if (!c.isSingleton()) { // throw new ClassCastException("Cannot set scalar value for array column " + col); // } if (c.isLogical()) { Boolean b = null; if (!Double.isNaN(value.doubleValue())) { b = value.longValue() != 0; } setTableElement(row, col, new byte[] {FitsEncoder.byteForBoolean(b)}); return; } Class base = c.getLegacyBase(); // quantize / unquantize as necessary... Quantizer q = c.getQuantizer(); if (q != null) { boolean decimalBase = (base == float.class || base == double.class); boolean decimalValue = (value instanceof Float || value instanceof Double || value instanceof BigInteger || value instanceof BigDecimal); if (decimalValue && !decimalBase) { value = q.toLong(value.doubleValue()); } else if (!decimalValue && decimalBase) { value = q.toDouble(value.longValue()); } } Object wrapped = null; if (base == byte.class) { wrapped = new byte[] {value.byteValue()}; } else if (base == short.class) { wrapped = new short[] {value.shortValue()}; } else if (base == int.class) { wrapped = new int[] {value.intValue()}; } else if (base == long.class) { wrapped = new long[] {value.longValue()}; } else if (base == float.class) { wrapped = new float[] {value.floatValue()}; } else if (base == double.class) { wrapped = new double[] {value.doubleValue()}; } else { // This could be a char based column... throw new ClassCastException("Cannot set number value for column of type " + base); } setTableElement(row, col, wrapped); } /** * Sets a boolean scalar table entry to the specified value. * * @param row the zero-based row index * @param col the zero-based column index * @param value the new boolean value * * @throws ClassCastException if the specified column in not a boolean scalar type. * @throws FitsException if the table element could not be altered * * @see #getLogical(int, int) * @see #set(int, int, Object) * * @since 1.18 */ private void setLogical(int row, int col, Boolean value) throws FitsException, ClassCastException { ColumnDesc c = columns.get(col); // Already checked before calling... // if (!c.isSingleton()) { // throw new ClassCastException("Cannot set scalar value for array column " + col); // } if (c.isLogical()) { setTableElement(row, col, new byte[] {FitsEncoder.byteForBoolean(value)}); } else if (c.getLegacyBase() == char.class) { setTableElement(row, col, new char[] {value == null ? '\0' : (value ? 'T' : 'F')}); } else { setNumber(row, col, value == null ? Double.NaN : (value ? 1 : 0)); } } /** * Sets a Unicode character scalar table entry to the specified value. * * @param row the zero-based row index * @param col the zero-based column index * @param value the new Unicode character value * * @throws ClassCastException if the specified column in not a boolean scalar type. * @throws FitsException if the table element could not be altered * * @see #getString(int, int) * * @since 1.18 */ private void setCharacter(int row, int col, Character value) throws FitsException, ClassCastException { ColumnDesc c = columns.get(col); // Already checked before calling... // if (!c.isSingleton()) { // throw new IllegalArgumentException("Cannot set scalar value for array column " + col); // } if (c.isLogical()) { setLogical(row, col, FitsUtil.parseLogical(value.toString())); } else if (c.fitsBase == char.class) { setTableElement(row, col, new char[] {value}); } else if (c.fitsBase == byte.class) { setTableElement(row, col, new byte[] {(byte) (value & FitsIO.BYTE_MASK)}); } else { throw new ClassCastException("Cannot convert char value to " + c.fitsBase.getName()); } } /** * Sets a table entry to the specified string value. Scalar column will attempt to parse the value, while * byte[] and char[] type columns will convert the string provided the string's length * does not exceed the entry size for these columns (the array elements will be padded with zeroes). Note, that * scalar byte columns will parse the string as a number (not as a single ASCII character). * * @param row the zero-based row index * @param col the zero-based column index * @param value the new boolean value * * @throws ClassCastException if the specified column is not a scalar type, and neither it is a * byte[] or char[] column. * @throws IllegalArgumentException if the String is too long to contain in the column. * @throws NumberFormatException if the numerical value could not be parsed. * @throws FitsException if the table element could not be altered * * @see #getString(int, int) * @see #set(int, int, Object) * * @since 1.18 */ private void setString(int row, int col, String value) throws FitsException, ClassCastException, IllegalArgumentException, NumberFormatException { ColumnDesc c = columns.get(col); // Already checked before calling... // if (!c.isSingleton()) { // throw new IllegalArgumentException("Cannot set scalar value for array column " + col); // } if (c.isLogical()) { setLogical(row, col, FitsUtil.parseLogical(value)); } else if (value.length() == 1) { setCharacter(row, col, value.charAt(0)); } else if (c.fitsDimension() > 1) { throw new ClassCastException("Cannot convert String to multi-dimensional array"); } else if (c.fitsDimension() == 1) { if (c.fitsBase != char.class && c.fitsBase != byte.class) { throw new ClassCastException("Cannot cast String to " + c.fitsBase.getName()); } int len = c.isVariableSize() ? value.length() : c.fitsCount; if (value.length() > len) { throw new IllegalArgumentException("String size " + value.length() + " exceeds entry size of " + len); } if (c.fitsBase == char.class) { setTableElement(row, col, Arrays.copyOf(value.toCharArray(), len)); } else { setTableElement(row, col, FitsUtil.stringToByteArray(value, len)); } } else { try { setNumber(row, col, Long.parseLong(value)); } catch (NumberFormatException e) { setNumber(row, col, Double.parseDouble(value)); } } } /** * @deprecated (for internal use) It may be reduced to private visibility in the future. Sets a * column with the data already flattened. * * @param col The index of the column to be replaced. * @param data The new data array. This should be a one-d primitive array. * * @throws FitsException Thrown if the type of length of the replacement data differs from the original. */ public synchronized void setFlattenedColumn(int col, Object data) throws FitsException { ensureData(); Object oldCol = table.getColumn(col); if (data.getClass() != oldCol.getClass() || Array.getLength(data) != Array.getLength(oldCol)) { throw new TableException("Replacement column mismatch at column:" + col); } table.setColumn(col, javaToFits1D(columns.get(col), data)); } @Override public void setRow(int row, Object[] data) throws FitsException { ensureData(); if (data.length != getNCols()) { throw new TableException("Mismatched number of columns: " + data.length + ", expected " + getNCols()); } for (int col = 0; col < data.length; col++) { set(row, col, data[col]); } } /** * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to (re)wrap tables in a * new HDU after column deletions, and then edit the new header as necessary to incorporate custom * entries. May be removed from the API in the future. */ @Override public synchronized void updateAfterDelete(int oldNcol, Header hdr) throws FitsException { hdr.addValue(Standard.NAXIS1, rowLen); int l = 0; for (ColumnDesc d : columns) { d.offset = l; l += d.rowLen(); } } @SuppressWarnings("resource") @Override public void write(ArrayDataOutput os) throws FitsException { synchronized (this) { try { if (isDeferred() && os == getRandomAccessInput()) { // It it's a deferred mode re-write, then data were edited in place if at all, // so we can skip the main table. ((RandomAccess) os).skipAllBytes(getRegularTableSize()); } else { // otherwise make sure we loaded all data before writing to the output ensureData(); // Write the regular table (if any) if (getRegularTableSize() > 0) { table.write(os); } } // Now check if we need to write the heap if (getParameterSize() > 0) { for (long rem = getHeapOffset(); rem > 0;) { byte[] b = new byte[(int) Math.min(getHeapOffset(), 1 << Short.SIZE)]; os.write(b); rem -= b.length; } getHeap().write(os); if (heapReserve > 0) { byte[] b = new byte[heapReserve]; os.write(b); } } FitsUtil.pad(os, getTrueSize(), (byte) 0); } catch (IOException e) { throw new FitsException("Unable to write table:" + e, e); } } } /** * Returns the heap offset component from a pointer. * * @param p the pointer, either a int[2] or a long[2]. * * @return the offset component from the pointer */ private long getPointerOffset(Object p) { return (p instanceof long[]) ? ((long[]) p)[1] : ((int[]) p)[1]; } /** * Returns the number of elements reported in a heap pointer. * * @param p the pointer, either a int[2] or a long[2]. * * @return the element count component from the pointer */ private long getPointerCount(Object p) { return (p instanceof long[]) ? ((long[]) p)[0] : ((int[]) p)[0]; } /** * Puts a FITS data array onto our heap, returning its locator pointer. The data will overwrite the previous heap * entry, if provided, so long as the new data fits in the same place. Otherwise the new data is placed at the end * of the heap. * * @param c The column descriptor, specifying the data type * @param o The variable-length data * @param p The heap pointer, where this element was stored on the heap before, or null if * we aren't replacing an earlier entry. * * @return the heap pointer information, either int[2] or else a long[2] * * @throws FitsException if the data could not be accessed in full from the heap. */ private Object putOnHeap(ColumnDesc c, Object o, Object oldPointer) throws FitsException { return putOnHeap(getHeap(), c, o, oldPointer); } /** * Puts a FITS data array onto a specific heap, returning its locator pointer. The data will overwrite the previous * heap entry, if provided, so long as the new data fits in the same place. Otherwise the new data is placed at the * end of the heap. * * @param h The heap object to use. * @param c The column descriptor, specifying the data type * @param o The variable-length data in Java form. * @param p The heap pointer, where this element was stored on the heap before, or null if * we aren't replacing an earlier entry. * * @return the heap pointer information, either int[2] or else a long[2] * * @throws FitsException if the data could not be accessed in full from the heap. */ private Object putOnHeap(FitsHeap h, ColumnDesc c, Object o, Object oldPointer) throws FitsException { // Flatten data for heap o = ArrayFuncs.flatten(o); // By default put data at the end of the heap; int off = h.size(); // The number of Java elements is the same as the number of FITS elements, except for strings and complex // numbers int len = (c.isComplex() || c.isString()) ? -1 : Array.getLength(o); // Convert to FITS storage array o = javaToFits1D(c, o); // For complex values and strings, determine length from converted object.... if (len < 0) { len = Array.getLength(o); // If complex in primitive 1D form, then length is half the number of elements. if (c.isComplex() && o.getClass().getComponentType().isPrimitive()) { len >>>= 1; } } if (oldPointer != null) { if (len <= getPointerCount(oldPointer)) { // Write data back at the old heap location off = (int) getPointerOffset(oldPointer); } } h.putData(o, off); return c.hasLongPointers() ? new long[] {len, off} : new int[] {len, off}; } /** * Returns a FITS data array from the heap * * @param c The column descriptor, specifying the data type * @param p The heap pointer, either int[2] or else a long[2] * @param isEnhanced Whether logicals should be returned as {@link Boolean} (rather than boolean) * and complex values as {@link ComplexValue} (rather than float[2] or * double[2]), or arrays thereof. Methods prior to 1.18 should set this to * false for back compatible behavior. * * @return the FITS array object retrieved from the heap * * @throws FitsException if the data could not be accessed in full from the heap. */ protected Object getFromHeap(ColumnDesc c, Object p, boolean isEnhanced) throws FitsException { long len = getPointerCount(p); long off = getPointerOffset(p); if (off > Integer.MAX_VALUE || len > Integer.MAX_VALUE) { throw new FitsException("Data located beyond 32-bit accessible heap limit: off=" + off + ", len=" + len); } Object e = null; if (c.isComplex()) { e = Array.newInstance(c.getFitsBase(), (int) len, 2); } else { e = Array.newInstance(c.getFitsBase(), c.getFitsBaseCount((int) len)); } readHeap(off, e); return fitsToJava1D(c, e, (int) len, isEnhanced); } /** * Convert Java arrays to their FITS representation. Transformation include boolean → 'T'/'F' or '\0'; * Strings → byte arrays; variable length arrays → pointers (after writing data to heap). * * @param c The column descritor * @param o A one-dimensional Java array * * @return An one-dimensional array with values as stored in FITS. * * @throws FitsException if the operation failed */ private static Object javaToFits1D(ColumnDesc c, Object o) throws FitsException { if (c.isBits()) { if (o instanceof Boolean && c.isSingleton()) { // Scalar boxed boolean... return FitsUtil.bitsToBytes(new boolean[] {(Boolean) o}); } return FitsUtil.bitsToBytes((boolean[]) o); } if (c.isLogical()) { // Convert true/false to 'T'/'F', or null to '\0' return FitsUtil.booleansToBytes(o); } if (c.isComplex()) { if (o instanceof ComplexValue || o instanceof ComplexValue[]) { return ArrayFuncs.complexToDecimals(o, c.fitsBase); } } if (c.isString()) { // Convert strings to array of bytes. if (o == null) { if (c.isVariableSize()) { return new byte[0]; } return Array.newInstance(byte.class, c.fitsShape); } if (o instanceof String) { int l = c.getStringLength(); if (l < 0) { // Not fixed width, write the whole string. l = ((String) o).length(); } return FitsUtil.stringToByteArray((String) o, l); } if (c.isVariableSize() && c.delimiter != 0) { // Write variable-length string arrays in delimited form for (String s : (String[]) o) { // We set the string length to that of the longest element + 1 c.setStringLength(Math.max(c.stringLength, s == null ? 1 : s.length() + 1)); } return FitsUtil.stringsToDelimitedBytes((String[]) o, c.getStringLength(), c.delimiter); } // Fixed length substring array (not delimited). // For compatibility with tools that do not process array dimension, ASCII NULL should not // be used between components (permissible only at the end of all strings) return FitsUtil.stringsToByteArray((String[]) o, c.getStringLength(), FitsUtil.BLANK_SPACE); } return ArrayFuncs.objectToArray(o, true); } /** * Converts from the FITS representation of data to their basic Java array representation. * * @param c The column descritor * @param o A one-dimensional array of values as stored in FITS * @param bits A bit count for bit arrays (otherwise unused). * @param isEnhanced Whether logicals should be returned as {@link Boolean} (rather than boolean) * and complex values as {@link ComplexValue} (rather than float[2] or * double[2]), or arrays thereof. Methods prior to 1.18 should set this to * false for back compatible behavior. * * @return A {@link String} or a one-dimensional array with the matched basic Java type * * @throws FitsException if the operation failed */ private Object fitsToJava1D(ColumnDesc c, Object o, int bits, boolean isEnhanced) { if (c.isBits()) { return FitsUtil.bytesToBits((byte[]) o, bits); } if (c.isLogical()) { return isEnhanced ? FitsUtil.bytesToBooleanObjects(o) : FitsUtil.byteToBoolean((byte[]) o); } if (c.isComplex() && isEnhanced) { return ArrayFuncs.decimalsToComplex(o); } if (c.isString()) { byte[] bytes = (byte[]) o; int len = c.getStringLength(); if (c.isVariableSize()) { if (c.delimiter != 0) { // delimited array of strings return FitsUtil.delimitedBytesToStrings(bytes, c.getStringLength(), c.delimiter); } } // If fixed or variable length arrays of strings... if (c.isSingleton()) { // Single fixed string -- get it all but trim trailing spaces return FitsUtil.extractString(bytes, new ParsePosition(0), bytes.length, FitsUtil.ASCII_NULL); } // Array of fixed-length strings -- we trim trailing spaces in each component String[] s = new String[bytes.length / len]; for (int i = 0; i < s.length; i++) { s[i] = FitsUtil.extractString(bytes, new ParsePosition(i * len), len, FitsUtil.ASCII_NULL); } return s; } return o; } /** * Create a column table with the specified number of rows. This is used when we defer instantiation of the * ColumnTable until the user requests data from the table. * * @param rows the number of rows to allocate * * @throws FitsException if the operation failed */ protected synchronized void createTable(int rows) throws FitsException { int nfields = columns.size(); Object[] data = new Object[nfields]; int[] sizes = new int[nfields]; for (int i = 0; i < nfields; i++) { ColumnDesc c = columns.get(i); sizes[i] = c.getTableBaseCount(); data[i] = c.newInstance(rows); } table = createColumnTable(data, sizes); nRow = rows; } /** * Sets the input to use for reading (and possibly writing) this table. If the input implements * {@link ReadWriteAccess}, then it can be used for both reading and (re)writing the data, including editing in * deferred mode. * * @param in The input from which we can read the table data. */ private void setInput(ArrayDataInput in) { encoder = (in instanceof ReadWriteAccess) ? new FitsEncoder((ReadWriteAccess) in) : null; } @Override public void read(ArrayDataInput in) throws FitsException { setInput(in); super.read(in); } @Override protected void loadData(ArrayDataInput in) throws IOException, FitsException { setInput(in); synchronized (this) { createTable(nRow); } readTrueData(in); } /** * Extracts a column descriptor from the FITS header for a given column index * * @param header the FITS header containing the column description(s) * @param col zero-based column index * * @return the Descriptor for that column. * * @throws FitsException if the header deswcription is invalid or incomplete */ public static ColumnDesc getDescriptor(Header header, int col) throws FitsException { String tform = header.getStringValue(Standard.TFORMn.n(col + 1)); if (tform == null) { throw new FitsException("Missing TFORM" + (col + 1)); } int count = 1; char type = 0; ParsePosition pos = new ParsePosition(0); try { count = AsciiFuncs.parseInteger(tform, pos); } catch (Exception e) { // Keep going... } try { type = Character.toUpperCase(AsciiFuncs.extractChar(tform, pos)); } catch (Exception e) { throw new FitsException("Missing data type in TFORM: [" + tform + "]"); } ColumnDesc c = new ColumnDesc(); if (header.containsKey(Standard.TTYPEn.n(col + 1))) { c.name(header.getStringValue(Standard.TTYPEn.n(col + 1))); } if (type == POINTER_INT || type == POINTER_LONG) { // Variable length column... c.setVariableSize(type == POINTER_LONG); // Get the data type... try { type = Character.toUpperCase(AsciiFuncs.extractChar(tform, pos)); } catch (Exception e) { throw new FitsException("Missing variable-length data type in TFORM: [" + tform + "]"); } } // The special types... if (type == 'C' || type == 'M') { c.isComplex = true; } else if (type == 'X') { c.isBits = true; } if (!c.setFitsType(type)) { throw new FitsException("Invalid type '" + type + "' in column:" + col); } if (!c.isVariableSize()) { // Fixed sized column... int[] dims = parseTDims(header.getStringValue(Standard.TDIMn.n(col + 1))); if (dims == null) { c.setFitsShape((count == 1 && type != 'A') ? SINGLETON_SHAPE : new int[] {count}); c.stringLength = -1; // T.B.D. further below... } else { c.setFitsShape(dims); } } if (c.isString()) { // For vairable-length columns or of TDIM was not defined determine substring length from TFORM. c.parseSubstringConvention(tform, pos, c.getStringLength() < 0); } // Force to use the count in the header, even if it does not match up with the dimension otherwise. c.fitsCount = count; c.quant = Quantizer.fromTableHeader(header, col); if (c.quant.isDefault()) { c.quant = null; } return c; } /** * Process one column from a FITS Header. * * @throws FitsException if the operation failed */ private int processCol(Header header, int col, int offset) throws FitsException { ColumnDesc c = getDescriptor(header, col); c.offset = offset; columns.add(c); return c.rowLen(); } /** * @deprecated (for internal use) Used Only by {@link nom.tam.image.compression.hdu.CompressedTableData} so * it would make a better private method in there.. ` */ protected void addByteVaryingColumn() { addColumn(ColumnDesc.createForVariableSize(byte.class)); } /** * @deprecated (for internal use) This method should have visibility reduced to private */ @SuppressWarnings("javadoc") protected ColumnTable createColumnTable(Object[] arrCol, int[] sizes) throws TableException { return new ColumnTable<>(arrCol, sizes); } /** * Returns the heap, after initializing it from the input as necessary * * @return the initialized heap * * @throws FitsException if we had trouble initializing it from the input. */ @SuppressWarnings("resource") private synchronized FitsHeap getHeap() throws FitsException { if (heap == null) { readHeap(getRandomAccessInput()); } return heap; } /** * Reads an array from the heap. Subclasses may override this, for example to provide read-only access to a related * table's heap area. * * @param offset the heap offset * @param array the array to populate from the heap area * * @throws FitsException if there was an issue accessing the heap */ protected void readHeap(long offset, Object array) throws FitsException { getHeap().getData((int) offset, array); } /** * Read the heap which contains the data for variable length arrays. A. Kovacs (4/1/08) Separated heap reading, s.t. * the heap can be properly initialized even if in deferred read mode. columnToArray() checks and initializes the * heap as necessary. * * @param input stream to read from. * * @throws FitsException if the heap could not be read from the stream * * @deprecated (for internal use) unused. */ protected synchronized void readHeap(ArrayDataInput input) throws FitsException { if (input instanceof RandomAccess) { FitsUtil.reposition(input, getFileOffset() + getHeapAddress()); } heap = new FitsHeap(heapFileSize); if (input != null) { heap.read(input); } } /** * Read table, heap and padding * * @param i the stream to read the data from. * * @throws FitsException if the reading failed */ protected synchronized void readTrueData(ArrayDataInput i) throws FitsException { try { table.read(i); i.skipAllBytes(getHeapOffset()); if (heap == null) { readHeap(i); } } catch (IOException e) { throw new FitsException("Error reading binary table data:" + e, e); } } /** * Check if the column number is valid. * * @param j The Java index (first=0) of the column to check. * * @return true if the column is valid */ protected boolean validColumn(int j) { return j >= 0 && j < getNCols(); } /** * Check to see if this is a valid row. * * @param i The Java index (first=0) of the row to check. * * @return true if the row is valid */ protected boolean validRow(int i) { return getNRows() > 0 && i >= 0 && i < getNRows(); } /** * @deprecated (for internal use) Visibility should be reduced to protected. */ @Override public void fillHeader(Header h) throws FitsException { fillHeader(h, true); } /** * Fills (updates) the essential header description of this table in the header, optionally updating the essential * column descriptions also if desired. * * @param h The FITS header to populate * @param updateColumns Whether to update the essential column descriptions also * * @throws FitsException if there was an error accessing the header. */ void fillHeader(Header h, boolean updateColumns) throws FitsException { h.deleteKey(Standard.SIMPLE); h.deleteKey(Standard.EXTEND); Standard.context(BinaryTable.class); Cursor c = h.iterator(); c.add(HeaderCard.create(Standard.XTENSION, Standard.XTENSION_BINTABLE)); c.add(HeaderCard.create(Standard.BITPIX, Bitpix.BYTE.getHeaderValue())); c.add(HeaderCard.create(Standard.NAXIS, 2)); synchronized (this) { c.add(HeaderCard.create(Standard.NAXIS1, rowLen)); c.add(HeaderCard.create(Standard.NAXIS2, nRow)); } if (h.getLongValue(Standard.PCOUNT, -1L) < getParameterSize()) { c.add(HeaderCard.create(Standard.PCOUNT, getParameterSize())); } c.add(HeaderCard.create(Standard.GCOUNT, 1)); c.add(HeaderCard.create(Standard.TFIELDS, columns.size())); if (getHeapOffset() == 0) { h.deleteKey(Standard.THEAP); } else { c.add(HeaderCard.create(Standard.THEAP, getHeapAddress())); } if (updateColumns) { for (int i = 0; i < columns.size(); i++) { c.setKey(Standard.TFORMn.n(i + 1).key()); fillForColumn(h, c, i); } } Standard.context(null); } /** * Update the header to reflect the details of a given column. * * @throws FitsException if the operation failed */ void fillForColumn(Header header, Cursor hc, int col) throws FitsException { ColumnDesc c = columns.get(col); try { Standard.context(BinaryTable.class); if (c.name() != null) { hc.add(HeaderCard.create(Standard.TTYPEn.n(col + 1), c.name())); } hc.add(HeaderCard.create(Standard.TFORMn.n(col + 1), c.getTFORM())); String tdim = c.getTDIM(); if (tdim != null) { hc.add(HeaderCard.create(Standard.TDIMn.n(col + 1), tdim)); } if (c.quant != null) { c.quant.editTableHeader(header, col); } } finally { Standard.context(null); } } /** * Returns the column descriptor of a given column in this table * * @param column the zero-based column index * * @return the column's descriptor * * @throws ArrayIndexOutOfBoundsException if this table does not contain a column with that index. * * @see #getDescriptor(String) */ public ColumnDesc getDescriptor(int column) throws ArrayIndexOutOfBoundsException { return columns.get(column); } /** * Returns the (first) column descriptor whose name matches the specified value. * * @param name The column name (case sensitive). * * @return The descriptor of the first column by that name, or null if the table contains no * column by that name. * * @see #getDescriptor(int) * @see #indexOf(String) * * @since 1.20 * * @author Attila Kovacs */ public ColumnDesc getDescriptor(String name) { int col = indexOf(name); return col < 0 ? null : getDescriptor(col); } /** * Converts a column from FITS logical values to bits. Null values (allowed in logical columns) will map to * false. * * @param col The zero-based index of the column to be reset. * * @return Whether the conversion was possible. * * * @since 1.18 */ public boolean convertToBits(int col) { ColumnDesc c = columns.get(col); if (c.isBits) { return true; } if (c.base != boolean.class) { return false; } c.isBits = true; return true; } /** * Convert a column from float/double to float complex/double complex. This is only possible for certain columns. * The return status indicates if the conversion is possible. * * @param index The zero-based index of the column to be reset. * * @return Whether the conversion is possible. * * * @throws FitsException if the operation failed * * @since 1.18 * * @see ColumnDesc#isComplex() * @see #addComplexColumn(Object, Class) */ public synchronized boolean setComplexColumn(int index) throws FitsException { if (!validColumn(index)) { return false; } ColumnDesc c = columns.get(index); if (c.isComplex()) { return true; } if (c.base != float.class && c.base != double.class) { return false; } if (!c.isVariableSize()) { if (c.getLastFitsDim() != 2) { return false; } // Set the column to complex c.isComplex = true; // Update the legacy (wrapped array) shape c.setLegacyShape(c.fitsShape); return true; } // We need to make sure that for every row, there are // an even number of elements so that we can // convert to an integral number of complex numbers. for (int i = 1; i < nRow; i++) { if (getPointerCount(getRawElement(i, index)) % 2 != 0) { return false; } } // Halve the length component of array descriptors (2 reals = 1 complex) for (int i = 1; i < nRow; i++) { Object p = getRawElement(i, index); long len = getPointerCount(p) >>> 1; if (c.hasLongPointers()) { ((long[]) p)[0] = len; } else { ((int[]) p)[0] = (int) len; } setTableElement(i, index, p); } // Set the column to complex c.isComplex = true; return true; } /** * Checks if this table contains a heap for storing variable length arrays (VLAs). * * @return true if the table contains a heap, or else false. * * @since 1.19.1 */ public final boolean containsHeap() { return getParameterSize() > 0; } /** *

* Defragments the heap area of this table, compacting the heap area, and returning the number of bytes by which the * heap size has been reduced. When tables with variable-sized columns are modified, the heap may retain old data as * columns are removed or elements get replaced with new data of different size. The data order in the heap may also * get jumbled, causing what would appear to be sequential reads to jump all over the heap space with the caching. * And, depending on how the heap was constructed in the first place, it may not be optimal for the row-after-row * table access that is the most typical use case. *

*

* This method rebuilds the heap by taking elements in table read order (by rows, and columns) and puts them on a * new heap. *

*

* For best squential read performance, you should defragment all tables that have been built column-by-column * before writing them to a FITS file. The only time defragmentation is really not needed is if the table was built * row-by-row, with no modifications to variable-length content after the fact. *

* * @return the number of bytes by which the heap has shrunk as a result of defragmentation. * * @throws FitsException if there was an error accessing the heap or the main data table comntaining the heap * locators. In case of an error the table content may be left in a damaged state. * * @see #compact() * @see #setElement(int, int, Object) * @see #addColumn(Object) * @see #deleteColumns(int, int) * @see #setColumn(int, Object) * * @since 1.18 */ public synchronized long defragment() throws FitsException { if (!containsHeap()) { return 0L; } int[] eSize = new int[columns.size()]; for (int j = 0; j < columns.size(); j++) { ColumnDesc c = columns.get(j); if (c.isVariableSize()) { eSize[j] = ElementType.forClass(c.getFitsBase()).size(); } } FitsHeap hp = getHeap(); long oldSize = hp.size(); FitsHeap compact = new FitsHeap(0); for (int i = 0; i < nRow; i++) { for (int j = 0; j < columns.size(); j++) { ColumnDesc c = columns.get(j); if (c.isVariableSize()) { Object p = getRawElement(i, j); int len = (int) getPointerCount(p); // Copy to new heap... int pos = compact.copyFrom(hp, (int) getPointerOffset(p), c.getFitsBaseCount(len) * eSize[j]); // Same length as before... if (p instanceof long[]) { ((long[]) p)[1] = pos; } else { ((int[]) p)[1] = pos; } // Update pointers in table setTableElement(i, j, p); } } } heap = compact; return oldSize - compact.size(); } /** * Discard the information about the original heap size (if this table was read from an input), and instead use the * real size of the actual heap (plus reserved space around it) when writing to an output. Compacted tables may not * be re-writeable to the same file from which they were read, since they may be shorter than the original, but they * can always be written to a different file, which may at times be smaller than the original. It may be used along * with {@link #defragment()} to create FITS files with optimized storage from FITS files that may contain wasted * space. * * @see #defragment() * * @since 1.19.1 * * @author Attila Kovacs */ public synchronized void compact() { heapFileSize = 0; } @Override public BinaryTableHDU toHDU() throws FitsException { Header h = new Header(); fillHeader(h); return new BinaryTableHDU(h, this); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/BinaryTableHDU.java000066400000000000000000000327031476377620500245070ustar00rootroot00000000000000package nom.tam.fits; import java.io.PrintStream; import nom.tam.fits.header.IFitsHeader; import nom.tam.fits.header.Standard; import nom.tam.util.ArrayDataOutput; import nom.tam.util.ArrayFuncs; import nom.tam.util.ColumnTable; import nom.tam.util.Cursor; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.fits.header.Standard.NAXIS1; import static nom.tam.fits.header.Standard.NAXIS2; import static nom.tam.fits.header.Standard.TDIMn; import static nom.tam.fits.header.Standard.TDISPn; import static nom.tam.fits.header.Standard.TFIELDS; import static nom.tam.fits.header.Standard.TFORMn; import static nom.tam.fits.header.Standard.TNULLn; import static nom.tam.fits.header.Standard.TSCALn; import static nom.tam.fits.header.Standard.TTYPEn; import static nom.tam.fits.header.Standard.TUNITn; import static nom.tam.fits.header.Standard.TZEROn; import static nom.tam.fits.header.Standard.XTENSION; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Binary table header/data unit. * * @see BinaryTable * @see AsciiTableHDU */ @SuppressWarnings("deprecation") public class BinaryTableHDU extends TableHDU { /** The standard column keywords for a binary table. */ private static final IFitsHeader[] KEY_STEMS = {TTYPEn, TFORMn, TUNITn, TNULLn, TSCALn, TZEROn, TDISPn, TDIMn}; /** * Creates a new binary table HDU from the specified FITS header and associated table data * * @deprecated (for internal use) Its visibility should be reduced to package level in the future. * * @param hdr the FITS header describing the data and any user-specific keywords * @param datum the corresponding data object */ public BinaryTableHDU(Header hdr, BinaryTable datum) { super(hdr, datum); } /** * Wraps the specified table in an HDU, creating a header for it with the essential table description. Users may * want to complete the table description with optional FITS keywords such as TTYPEn, * TUNITn etc. It is strongly recommended that the table structure (rows or columns) isn't altered * after the table is encompassed in an HDU, since there is no guarantee that the header description will be kept in * sync. * * @param tab the binary table to wrap into a new HDU * * @return A new HDU encompassing and describing the supplied table. * * @throws FitsException if the table structure is invalid, and cannot be described in a header (should never really * happen, but we keep the possibility open to it). * * @see BinaryTable#toHDU() * * @since 1.18 */ public static BinaryTableHDU wrap(BinaryTable tab) throws FitsException { BinaryTableHDU hdu = new BinaryTableHDU(new Header(), tab); tab.fillHeader(hdu.myHeader); return hdu; } @Override protected final String getCanonicalXtension() { return Standard.XTENSION_BINTABLE; } /** * @deprecated (for internal use) Use {@link BinaryTable#fromColumnMajor(Object[])} or * {@link BinaryTable#fromRowMajor(Object[][])} instead. Will reduce visibility in the * future. * * @return Encapsulate data in a BinaryTable data type * * @param o data to encapsulate * * @throws FitsException if the type of the data is not usable as data */ @Deprecated public static BinaryTable encapsulate(Object o) throws FitsException { if (o instanceof ColumnTable) { return new BinaryTable((ColumnTable) o); } if (o instanceof Object[][]) { return BinaryTable.fromRowMajor((Object[][]) o); } if (o instanceof Object[]) { return BinaryTable.fromColumnMajor((Object[]) o); } throw new FitsException("Unable to encapsulate object of type:" + o.getClass().getName() + " as BinaryTable"); } /** * Check if this data object is consistent with a binary table. * * @param o a column table object, an Object[][], or an Object[]. This routine doesn't check that the * dimensions of arrays are properly consistent. * * @return true if the data object can be represented as a FITS binary table, otherwise * false. * * @deprecated (for internal use) Will reduce visibility in the future */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isData(Object o) { return o instanceof nom.tam.util.ColumnTable || o instanceof Object[][] || o instanceof Object[]; } /** * Check that this is a valid binary table header. * * @deprecated (for internal use) Will reduce visibility in the future * * @param header to validate. * * @return true if this is a binary table header. */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isHeader(Header header) { String xten = header.getStringValue(XTENSION); if (xten == null) { return false; } xten = xten.trim(); return xten.equals(Standard.XTENSION_BINTABLE) || xten.equals("A3DTABLE"); } /** * Prepares a data object into which the actual data can be read from an input subsequently or at a later time. * * @deprecated (for internal use) Will reduce visibility in the future * * @param header The FITS header that describes the data * * @return A data object that support reading content from a stream. * * @throws FitsException if the data could not be prepared to prescriotion. */ @Deprecated public static BinaryTable manufactureData(Header header) throws FitsException { return new BinaryTable(header); } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return a newly created binary table HDU from the supplied data. * * @param data the data used to build the binary table. This is typically some kind of array of * objects. * * @throws FitsException if there was a problem with the data. */ @Deprecated public static Header manufactureHeader(Data data) throws FitsException { Header hdr = new Header(); data.fillHeader(hdr); return hdr; } @Override public int addColumn(Object data) throws FitsException { int n = myData.addColumn(data); myHeader.addValue(Standard.NAXISn.n(1), myData.getRowBytes()); Cursor c = myHeader.iterator(); c.end(); myData.fillForColumn(myHeader, c, n - 1); return super.addColumn(data); } /** * For internal use. Returns the FITS header key stems to use for describing binary tables. * * @return an array of standatd header colum knetwords stems. */ protected static IFitsHeader[] binaryTableColumnKeyStems() { return KEY_STEMS; } @Override protected IFitsHeader[] columnKeyStems() { return BinaryTableHDU.KEY_STEMS; } @Override public void info(PrintStream stream) { stream.println(" Binary Table"); stream.println(" Header Information:"); int nhcol = myHeader.getIntValue(TFIELDS, -1); int nrow = myHeader.getIntValue(NAXIS2, -1); int rowsize = myHeader.getIntValue(NAXIS1, -1); stream.print(" " + nhcol + " fields"); stream.println(", " + nrow + " rows of length " + rowsize); for (int i = 1; i <= nhcol; i++) { stream.print(" " + i + ":"); prtField(stream, "Name", TTYPEn.n(i).key()); prtField(stream, "Format", TFORMn.n(i).key()); prtField(stream, "Dimens", TDIMn.n(i).key()); stream.println(""); } stream.println(" Data Information:"); stream.println(" Number of rows=" + myData.getNRows()); stream.println(" Number of columns=" + myData.getNCols()); stream.println(" Heap size is: " + myData.getParameterSize() + " bytes"); Object[] cols = myData.getFlatColumns(); for (int i = 0; i < cols.length; i++) { stream.println(" " + i + ":" + ArrayFuncs.arrayDescription(cols[i])); } } /** * Check that this HDU has a valid header. * * @deprecated (for internal use) Will reduce visibility in the future * * @return true if this HDU has a valid header. */ @Deprecated public boolean isHeader() { return isHeader(myHeader); } private void prtField(PrintStream stream, String type, String field) { String val = myHeader.getStringValue(field); if (val != null) { stream.print(type + '=' + val + "; "); } } /** * Returns a copy of the column descriptor of a given column in this table * * @param col the zero-based column index * * @return a copy of the column's descriptor * * @see BinaryTable#getDescriptor(int) * * @since 1.18 */ public BinaryTable.ColumnDesc getColumnDescriptor(int col) { return myData.getDescriptor(col); } @Override public void setColumnName(int index, String name, String comment) throws IndexOutOfBoundsException, HeaderCardException { super.setColumnName(index, name, comment); getColumnDescriptor(index).name(name); } /** * Converts a column from FITS logical values to bits. Null values (allowed in logical columns) will map to * false. It is legal to call this on a column that is already containing bits. * * @param col The zero-based index of the column to be reset. * * @return Whether the conversion was possible. * * * @throws FitsException if the header could not be updated * * @since 1.18 */ public final boolean convertToBits(int col) throws FitsException { if (!myData.convertToBits(col)) { return false; } // Update TFORM keyword myHeader.getCard(Standard.TFORMn.n(col + 1)).setValue(getColumnDescriptor(col).getTFORM()); return true; } /** * Convert a column in the table to complex. Only tables with appropriate types and dimensionalities can be * converted. It is legal to call this on a column that is already complex. * * @param index The zero-based index of the column to be converted. * * @return Whether the column can be converted * * @throws FitsException if the header could not be updated * * @see BinaryTableHDU#setComplexColumn(int) */ public boolean setComplexColumn(int index) throws FitsException { if (!myData.setComplexColumn(index)) { return false; } // Update TFORM keyword myHeader.getCard(Standard.TFORMn.n(index + 1)).setValue(getColumnDescriptor(index).getTFORM()); // Update or remove existing TDIM keyword if (myHeader.containsKey(Standard.TDIMn.n(index + 1))) { String tdim = getColumnDescriptor(index).getTDIM(); if (tdim != null) { myHeader.getCard(Standard.TDIMn.n(index + 1)).setValue(tdim); } else { myHeader.deleteKey(Standard.TDIMn.n(index + 1)); } } return true; } // Need to tell header about the Heap before writing. @Override public void write(ArrayDataOutput out) throws FitsException { myData.fillHeader(myHeader, false); super.write(out); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/Data.java000066400000000000000000000370011476377620500226170ustar00rootroot00000000000000package nom.tam.fits; import java.io.EOFException; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.fits.utilities.FitsCheckSum; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.FitsInputStream; import nom.tam.util.RandomAccess; import static nom.tam.util.LoggerHelper.getLogger; /** * The data segment of an HDU. *

* This is the object which contains the actual data for the HDU. *

*
    *
  • For images and primary data this is a simple (but possibly multi-dimensional) primitive array. When group data is * supported it will be a possibly multidimensional array of group objects. *
  • For ASCII data it is a two dimensional Object array where each of the constituent objects is a primitive array of * length 1. *
  • For Binary data it is a two dimensional Object array where each of the constituent objects is a primitive array * of arbitrary (more or less) dimensionality. *
*/ @SuppressWarnings("deprecation") public abstract class Data implements FitsElement { private static final Logger LOG = getLogger(Data.class); private static final int FITS_BLOCK_SIZE_MINUS_ONE = FitsFactory.FITS_BLOCK_SIZE - 1; /** * @deprecated Will be private. Access via {@link #getFileOffset()} The starting location of the data when last read */ @Deprecated protected long fileOffset = -1; /** * @deprecated Will be removed. Use {@link #getTrueSize()} instead. The size of the data when last read */ @Deprecated protected long dataSize; /** * @deprecated Will be private. Use {@link #getRandomAccessInput()} instead. The input stream used. */ @Deprecated protected RandomAccess input; /** The data checksum calculated from the input stream */ private long streamSum = 0L; /** * Returns the random accessible input from which this data can be read, if any. * * @return the random access input from which we can read the data when needed, or null if this data * object is not associated to an input, or it is not random accessible. */ protected final RandomAccess getRandomAccessInput() { return input; } /** * Describe the structure of this data object in the supplied header. * * @param head header to fill with the data from the current data object * * @throws FitsException if the operation fails */ protected abstract void fillHeader(Header head) throws FitsException; /** * Checks if the data should be assumed to be in deferred read mode. * * @return true if it is set for deferred reading at a later time, or else false if this * data is currently loaded into RAM. #see {@link #detach()} * * @since 1.17 */ @SuppressWarnings("resource") public boolean isDeferred() { return getTrueSize() != 0 && isEmpty() && getRandomAccessInput() != null; } /** * Checks if the data content is currently empty, i.e. no actual data is currently stored in memory. * * @return true if there is no actual data in memory, otherwise false * * @see #isDeferred() * @see #getCurrentData() * * @since 1.18 */ public boolean isEmpty() { return getCurrentData() == null; } /** * Computes and returns the FITS checksum for this data, for example to compare against the stored * DATASUM in the FITS header (e.g. via {@link BasicHDU#getStoredDatasum()}). This method always * computes the checksum from data in memory. As such it will fully load deferred read mode data into RAM to perform * the calculation, and use the standard padding to complete the FITS block for the calculation. As such the * checksum may differ from that of the file if the file uses a non-standard padding. Hence, for verifying data * integrity as stored in a file {@link BasicHDU#verifyDataIntegrity()} or {@link BasicHDU#verifyIntegrity()} should * be preferred. * * @return the computed FITS checksum from the data (fully loaded in memory). * * @throws FitsException if there was an error while calculating the checksum * * @see BasicHDU#getStoredDatasum() * @see BasicHDU#verifyDataIntegrity() * @see BasicHDU#verifyIntegrity() * * @since 1.17 */ public long calcChecksum() throws FitsException { return FitsCheckSum.checksum(this); } /** * Returns the checksum value calculated duting reading from a stream. It always returns a value that is greater or * equal to zero. It is only populated when reading from {@link FitsInputStream} imputs, and never from other types * of inputs. The default return value is zero. * * @return the checksum calculated for the data read from a stream, or else zero if the data was not read from the * stream. * * @see FitsInputStream * @see Header#getStreamChecksum() * * @since 1.18.1 */ final long getStreamChecksum() { return streamSum; } /** * Returns the underlying Java representation of the data contained in this HDU's data segment. Typically it will * return a Java array of some kind. * * @return the underlying Java representation of the data core object, such as a multi-dimensional * Java array. * * @throws FitsException if the data could not be gathered. * * @see #isDeferred() * @see #ensureData() */ public Object getData() throws FitsException { ensureData(); return getCurrentData(); } /** * Returns the data content that is currently in memory. In case of a data object in deferred read state (that is * its prescription has been parsed from the header, but no data content was loaded yet from a random accessible * input), this call may return null or an object representing empty data. * * @return The current data content in memory. * * @see #getData() * @see #ensureData() * @see #isDeferred() * @see #isEmpty() * * @since 1.18 */ protected abstract Object getCurrentData(); @Override public long getFileOffset() { return fileOffset; } /** * Same as {@link #getData()}. * * @return The data content as represented by a Java object.. * * @throws FitsException if the data could not be gathered . */ public final Object getKernel() throws FitsException { return getData(); } @Override public long getSize() { return FitsUtil.addPadding(getTrueSize()); } /** * Returns the calculated byte size of the data, regardless of whether the data is currently in memory or not. * * @return the calculated byte size for the data. */ protected abstract long getTrueSize(); /** *

* Load data from the current position of the input into memory. This may be triggered immediately when calling * {@link #read(ArrayDataInput)} if called on a non random accessible input, or else later when data is accessed via * {@link #ensureData()}, for example as a result of a {@link #getData()} call. This method will not be called * unless there is actual data of non-zero size to be read. *

*

* Implementations should create appropriate data structures and populate them from the specified input. *

* * @param in The input from which to load data * * @throws IOException if the data could not be loaded from the input. * @throws FitsException if the data is garbled. * * @see #read(ArrayDataInput) * @see #ensureData() * @see #getData() * @see #isDeferred() * * @since 1.18 */ protected abstract void loadData(ArrayDataInput in) throws IOException, FitsException; private void skipPadding(ArrayDataInput in) throws PaddingException, FitsException { try { in.skipAllBytes((long) FitsUtil.padding(getTrueSize())); } catch (EOFException e) { throw new PaddingException("EOF while skipping padding after data segment", e); } catch (IOException e) { throw new FitsException("IO error while skipping padding after data segment", e); } } /** * Makes sure that data that may have been deferred earlier from a random access input is now loaded into memory. * * @throws FitsException if the deferred data could not be loaded. * * @see #getData() * @see #read(ArrayDataInput) * @see #isDeferred() * * @since 1.18 */ protected void ensureData() throws FitsException { if (!isDeferred()) { return; } try { long pos = input.getFilePointer(); input.seek(getFileOffset()); loadData(input); input.seek(pos); } catch (IOException e) { throw new FitsException("error reading deferred data: " + e, e); } } /** *

* Reads the data or skips over it for reading later, depending on whether reading from a stream or a random * acessible input, respectively. *

*

* In case the argument is a an instance of {@link RandomAccess} input (such as a {@link nom.tam.util.FitsFile}, the * call will simply note where in the file the data segment can be found for reading at a later point, only when the * data content is accessed. This 'deferred' reading behavior make it possible to process large HDUs even with small * amount of RAM, and can result in a significant performance boost when inspectring large FITS files, or using only * select content from large FITS files. *

* * @throws PaddingException if there is missing padding between the end of the data segment and the enf-of-file. * @throws FitsException if the data appears to be corrupted. * * @see #getData() * @see #ensureData() */ @Override public void read(ArrayDataInput in) throws PaddingException, FitsException { detach(); if (in == null) { return; } if (in instanceof FitsInputStream) { ((FitsInputStream) in).nextChecksum(); } streamSum = 0L; setFileOffset(in); if (getTrueSize() == 0) { return; } if (in instanceof RandomAccess) { // If random accessible, then defer reading.... try { in.skipAllBytes(getTrueSize()); } catch (IOException e) { throw new FitsException("Unable to skip over data segment:" + e, e); } } else { try { loadData(in); } catch (IOException e) { throw new FitsException("error reading data: " + e, e); } } skipPadding(in); if (in instanceof FitsInputStream) { streamSum = ((FitsInputStream) in).nextChecksum(); } } @SuppressWarnings("resource") @Override public boolean reset() { try { FitsUtil.reposition(getRandomAccessInput(), getFileOffset()); return true; } catch (Exception e) { LOG.log(Level.SEVERE, "Unable to reset", e); return false; } } @SuppressWarnings("resource") @Override public void rewrite() throws FitsException { if (isDeferred()) { return; // Nothing to do... } if (!rewriteable()) { throw new FitsException("Illegal attempt to rewrite data"); } FitsUtil.reposition(getRandomAccessInput(), getFileOffset()); write((ArrayDataOutput) getRandomAccessInput()); try { ((ArrayDataOutput) getRandomAccessInput()).flush(); } catch (IOException e) { throw new FitsException("Error in rewrite flush: ", e); } } @Override public boolean rewriteable() { return input != null && getFileOffset() >= 0 && (getTrueSize() + FITS_BLOCK_SIZE_MINUS_ONE) / FitsFactory.FITS_BLOCK_SIZE == (getTrueSize() + FITS_BLOCK_SIZE_MINUS_ONE) / FitsFactory.FITS_BLOCK_SIZE; } /** * Detaches this data object from the input (if any), such as a file or stream, but not before loading data from the * previously assigned input into memory. * * @throws FitsException if there was an issue loading the data from the previous input (if any) * * @see #isDeferred() * * @since 1.18 */ public void detach() throws FitsException { ensureData(); clearInput(); } private void clearInput() { input = null; fileOffset = -1; dataSize = 0L; } /** * Record the information necessary for eading the data content at a later time (deferred reading). * * @param o reread information. * * @see #isDeferred() */ protected void setFileOffset(ArrayDataInput o) { if (o instanceof RandomAccess) { fileOffset = FitsUtil.findOffset(o); dataSize = getTrueSize(); input = (RandomAccess) o; } else { clearInput(); } } @Override public abstract void write(ArrayDataOutput o) throws FitsException; /** * Returns an approprotae HDU object that encapsulates this FITS data, and contains the minimal mandatory header * description for that data. * * @throws FitsException If the data cannot be converted to an HDU for some reason. * * @return a HDU object ocntaining the data and its minimal required header description */ public abstract BasicHDU toHDU() throws FitsException; } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/Fits.java000066400000000000000000002137651476377620500226700ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.Closeable; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.fits.compress.CompressionManager; import nom.tam.fits.header.Standard; import nom.tam.fits.utilities.FitsCheckSum; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.FitsFile; import nom.tam.util.FitsIO; import nom.tam.util.FitsInputStream; import nom.tam.util.FitsOutputStream; import nom.tam.util.RandomAccess; import nom.tam.util.RandomAccessFileIO; import nom.tam.util.SafeClose; import static nom.tam.fits.header.Standard.EXTNAME; import static nom.tam.fits.header.Standard.EXTVER; /** *

* Handling of FITS files and streams. This class is a container of HDUs (header-data units), which together constitute * a complete FITS file. Users of this library are strongly encouraged to study the * FITS Standard documentation before using this library, as * the library typically requires a level of familiarity with FITS and its capabilities. When constructing FITS files, * users will typically want to populate their headers with as much of the standard information as possible to provide a * full and accurate description of the data they wish to represent way beyond the bare essentials that are handled * automatically by this library. *

*

* Fits objects can be built-up HDU-by-HDU, and then written to a file (or stream), e.g.: *

* *
 *   // Create a new empty Fits containe
 *   Fits fits = new Fits(); 
 *   
 *   // Create an image HDU, e.g. from a 2D array we have prepared earlier
 *   float[][] image = ...
 *   BasicHDU<?> imageHDU = Fits.makeHDU(image);
 *   
 *   // ... we can of course add data to the HDU's header as we like...
 *   
 *   // Make this image the first HDU...
 *   fits.addHDU(imageHDU); 
 *   
 *   // Write the FITS to a file...
 *   fits.write("myimage.fits");
 * 
*

* Or, we may read a Fits object from the input, e.g. as: *

* *
 *   // Create and empty Fits assigned to an input file
 *   Fits f = new Fits(new File("myimage.fits");
 *   
 *   // Read the entire FITS (skipping over the data for now...)
 *   f.read();
 *   
 *   // Get the image data from the first HDU (will actually read the image now)
 *   float[][] image = (float[][]) f.getHDU(0).getKernel();
 * 
*

* When reading FITS from random-accessible files (like in the example above), the {@link #read()} call will parse the * header for each HDU but will defer reading of actual data to a later time when it's actually accessed. This makes * Fits objects fast, frugal, and lean, especially when one is interested in certain parts of the data * contained in the FITS file. (When reading from streams, deferred reading is not an option, so {@link #read()} will * load all HDUs into memory each time). *

*

* Fits objects also allow reading HDUs sequentially one at a time using the {@link #readHDU()}, or even * when using {@link #getHDU(int)} or {@link #getHDU(String)} methods, even if {@link #read()} was not called * previously, e.g.: *

* *
 *   // Create and empty Fits assigned to an input
 *   Fits f = new Fits(new File("myimage.fits");
 *   
 *   // Get HDU index 2 (0-based, i.e. 3rd HDU) FITS. It will read (stream) or skim (file) the FITS up to the 3rd
 *   // HDU, returning it. If the FITS file or stream contains further HDUs they will not be accessed until we
 *   // need them later (if at all).
 *   BasucHDU<?> hdu = f.getHDU(2);
 * 
*

* When building Fits from local Java data objects, it's best to use {@link #makeHDU(Object)} to create * HDUs, which will chose the most appropriate type of HDU for the given data object (taking into some of the static * preferences set in FitsFactory prior). {@link #makeHDU(Object)} will return one of the following HDU * objects: *

    *
  • {@link NullDataHDU}
  • *
  • {@link ImageHDU}
  • *
  • {@link BinaryTableHDU}
  • *
  • {@link AsciiTableHDU}
  • *
  • {@link UndefinedHDU}
  • *
*

* all of which derive from {@link BasicHDU}. *

*

* Since HDU literally means 'header-data unit', they constitute of a header and data entities, which can be accessed * separately. The {@link Header} class provides many functions to add, delete and read header keywords in HDUs in a * variety of formats. The {@link Data} class, and its concrete subclassses provide access to the specific data object * that the HDU encapsulates. *

* * @see FitsFactory * * @version 1.21 */ @SuppressWarnings("deprecation") public class Fits implements Closeable { /** * logger to log to. */ private static final Logger LOG = Logger.getLogger(Fits.class.getName()); /** * The input stream associated with this Fits object. */ private ArrayDataInput dataStr; /** * A vector of HDUs that have been added to this Fits object. */ private final List> hduList = new ArrayList<>(); /** * Has the input stream reached the EOF? */ private boolean atEOF; /** * The last offset we reached. A -1 is used to indicate that we cannot use the offset. */ private long lastFileOffset = -1; /** * Creates an empty Fits object which is not associated with an input stream. */ public Fits() { } /** *

* Creates a new (empty) FITS container associated with a file input. If the file is compressed a stream will be * used, otherwise random access will be supported. *

*

* While the FITS object is associated with the specified file, it is initialized as an empty container with no data * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to * the container. *

* * @param myFile The File object. The content of this file will not be read into the Fits object until the * user makes some explicit request. * @throws FitsException if the operation failed * * @throws FitsException if the operation failed * * @see #Fits(FitsFile) * @see #Fits(RandomAccessFileIO) * @see #Fits(String) * @see #read() * @see #getHDU(int) * @see #readHDU() * @see #skipHDU() * @see #addHDU(BasicHDU) */ public Fits(File myFile) throws FitsException { this(myFile, CompressionManager.isCompressed(myFile)); } /** *

* Creates a new (empty) FITS container associated with a file input. *

*

* While the FITS object is associated with the specified file, it is initialized as an empty container with no data * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to * the container. *

* * @deprecated Use {@link #Fits(File)} instead (compression is auto detected). Will remove in the * future. * * @param myFile The File object. The content of this file will not be read into the Fits object until * the user makes some explicit request. * @param compressed Is the data compressed? * * @throws FitsException if the operation failed * * @see #Fits(File) */ public Fits(File myFile, boolean compressed) throws FitsException { fileInit(myFile, compressed); } /** *

* Creates a new (empty) FITS container associated with an input that supports generalized random access. *

*

* While the FITS object is associated with the specified input, it is initialized as an empty container with no * data loaded from the input automatically. You may want to call {@link #read()} to load all data from the input * and/or {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via * {@link #addHDU(BasicHDU)} to the container. *

* * @param src the random access input. The content of this input will not be read into the Fits object * until the user makes some explicit request. * * @throws FitsException if the operation failed * * @see #Fits(File) * @see #Fits(FitsFile) * @see #read() * @see #getHDU(int) * @see #readHDU() * @see #skipHDU() * @see #addHDU(BasicHDU) */ public Fits(RandomAccessFileIO src) throws FitsException { randomInit(src); } /** *

* Creates a new (empty) FITS container associated with {@link FitsFile} input. *

*

* While the FITS object is associated with the specified file input, it is initialized as an empty container with * no data loaded from the input automatically. You may want to call {@link #read()} to load all data from the input * and/or {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via * {@link #addHDU(BasicHDU)} to the container. *

* * @param src the random access input. The content of this input will not be read into the Fits object * until the user makes some explicit request. * * @throws FitsException if the input could not bew repositions to its beginning * * @see #Fits(File) * @see #Fits(RandomAccessFileIO) * @see #read() * @see #getHDU(int) * @see #readHDU() * @see #skipHDU() * @see #addHDU(BasicHDU) * * @since 1.18 */ public Fits(FitsFile src) throws FitsException { dataStr = src; try { src.seek(0); } catch (Exception e) { throw new FitsException("Could not create Fits: " + e.getMessage(), e); } } /** *

* Creates a new (empty) FITS container associated with the given input stream. Compression is determined from the * first few bytes of the stream. *

*

* While the FITS object is associated with the specified input stream, it is initialized as an empty container with * no data loaded from the input automatically. You may want to call {@link #read()} to load all data from the input * and/or {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via * {@link #addHDU(BasicHDU)} to the container. *

* * @param str The data stream. The content of this stream will not be read into the Fits object until the * user makes some explicit request. * * @throws FitsException if the operation failed * * @see #Fits(File) * @see #Fits(FitsFile) * @see #read() * @see #getHDU(int) * @see #readHDU() * @see #skipHDU() * @see #addHDU(BasicHDU) */ public Fits(InputStream str) throws FitsException { streamInit(str); } /** *

* Creates a new (empty) FITS container associated with an input stream. *

*

* While the FITS object is associated with the specified input stream, it is initialized as an empty container with * no data loaded from the input automatically. You may want to call {@link #read()} to load all data from the input * and/or {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via * {@link #addHDU(BasicHDU)} to the container. *

* * @param str The data stream. The content of this stream will not be read into the Fits object until * the user makes some explicit request. * @param compressed Is the stream compressed? This is currently ignored. Compression is determined from the * first two bytes in the stream. * * @throws FitsException if the operation failed * * @deprecated Use {@link #Fits(InputStream)} instead (compression is auto detected). Will remove in * the future. * * @see #Fits(InputStream) */ @Deprecated public Fits(InputStream str, boolean compressed) throws FitsException { this(str); LOG.log(Level.INFO, "compression ignored, will be autodetected. was set to " + compressed); } /** *

* Creates a new (empty) FITS container with a file or URL as its input. The string is assumed to be a URL if it * begins one of the protocol strings. If the string ends in .gz it is assumed that the data is in a compressed * format. All string comparisons are case insensitive. *

*

* While the FITS object is associated with the specified file, it is initialized as an empty container with no data * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to * the container. *

* * @param filename The name of the file or URL to be processed. The content of this file will not be read into * the Fits object until the user makes some explicit request. * * @throws FitsException Thrown if unable to find or open a file or URL from the string given. * * @see #Fits(URL) * @see #Fits(FitsFile) * @see #Fits(File) * @see #read() * @see #getHDU(int) * @see #readHDU() * @see #skipHDU() * @see #addHDU(BasicHDU) **/ public Fits(String filename) throws FitsException { this(filename, CompressionManager.isCompressed(filename)); } /** *

* Creates a new (empty) FITS container associated with a file or URL as its input. The string is assumed to be a * URL if it begins one of the protocol strings. If the string ends in .gz it is assumed that the data is in a * compressed format. All string comparisons are case insensitive. *

*

* While the FITS object is associated with the specified file, it is initialized as an empty container with no data * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to * the container. *

* * @param filename The name of the file or URL to be processed. The content of this file will not be read * into the Fits object until the user makes some explicit request. * @param compressed is the file compressed? * * @throws FitsException Thrown if unable to find or open a file or URL from the string given. * * @deprecated Use {@link #Fits(String)} instead (compression is auto detected). Will be a private * method in the future. * * @see #Fits(String) **/ @SuppressWarnings("resource") public Fits(String filename, boolean compressed) throws FitsException { if (filename == null) { throw new FitsException("Null FITS Identifier String"); } try { File fil = new File(filename); if (fil.exists()) { fileInit(fil, compressed); return; } } catch (Exception e) { LOG.log(Level.FINE, "not a file " + filename, e); throw new FitsException("could not detect type of " + filename, e); } try { InputStream str = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename); if (str != null) { streamInit(str); return; } } catch (Exception e) { LOG.log(Level.FINE, "not a resource " + filename, e); throw new FitsException("could not detect type of " + filename, e); } try { InputStream is = FitsUtil.getURLStream(new URL(filename), 0); streamInit(is); return; } catch (Exception e) { LOG.log(Level.FINE, "not a url " + filename, e); throw new FitsException("could not detect type of " + filename, e); } } /** *

* Creates a new (empty) FITS container with a given URL as its input. *

*

* While the FITS object is associated with the resource, it is initialized as an empty container with no data * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to * the container. *

* * @param myURL The URL to be read. The content of this URL will not be read into the Fits object until the * user makes some explicit request. * * @throws FitsException Thrown if unable to find or open a file or URL from the string given. * * @see #Fits(String) * @see #Fits(RandomAccessFileIO) * @see #read() * @see #getHDU(int) * @see #readHDU() * @see #skipHDU() * @see #addHDU(BasicHDU) */ @SuppressWarnings("resource") public Fits(URL myURL) throws FitsException { try { streamInit(FitsUtil.getURLStream(myURL, 0)); } catch (IOException e) { throw new FitsException("Unable to open input from URL:" + myURL, e); } } /** *

* Creates a new (empty) FITS container associated with a given uncompressed URL as its input. *

*

* While the FITS object is associated with the resource, it is initialized as an empty container with no data * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to * the container. *

* * @param myURL The URL to be associated with the FITS file. The content of this URL will not be read * into the Fits object until the user makes some explicit request. * @param compressed Compression flag, ignored. * * @throws FitsException Thrown if unable to use the specified URL. * * @deprecated Use {@link #Fits(URL)} instead (compression is auto detected). Will remove in the * future. * * @see #Fits(URL) */ @Deprecated public Fits(URL myURL, boolean compressed) throws FitsException { this(myURL); LOG.log(Level.INFO, "compression ignored, will be autodetected. was set to " + compressed); } /** * Creates a new empty HDU for the given data type. * * @return a newly created HDU from the given Data. * * @param data The data to be described in this HDU. * @param the class of the HDU * * @throws FitsException if the operation failed */ public static BasicHDU makeHDU(DataClass data) throws FitsException { Header hdr = new Header(); data.fillHeader(hdr); return FitsFactory.hduFactory(hdr, data); } /** * Creates a new empty HDU based on the header description of the data * * @return a newly created HDU from the given header (and including the header). * * @param h The header which describes the FITS extension * * @throws FitsException if the header could not be converted to a HDU. */ public static BasicHDU makeHDU(Header h) throws FitsException { Data d = FitsFactory.dataFactory(h); return FitsFactory.hduFactory(h, d); } /** *

* Creates an HDU that wraps around the specified data object. The HDUs header will be created and populated with * the essential description of the data. The following HDU types may be returned depending on the nature of the * argument: *

*
    *
  • {@link NullDataHDU} -- if the argument is null
  • *
  • {@link ImageHDU} -- if the argument is a regular numerical array, such as a double[], * float[][], or short[][][]
  • *
  • {@link BinaryTableHDU} -- the the argument is an Object[rows][cols] type array with a regular * structure and supported column data types, provided that it cannot be represented by an ASCII table OR if * {@link FitsFactory#getUseAsciiTables()} is false
  • *
  • {@link AsciiTableHDU} -- Like above, but only when the data can be represented by an ASCII table AND * {@link FitsFactory#getUseAsciiTables()} is true
  • *
*

* As of 1.18, this metohd will not create and return random group HDUs for Object[][2] style data. * Instead, it will return an appropriate binary or ASCII table, since the FITS standard recommends against using * random groups going forward, except for reading some old data from certain radio telescopes. If the need ever * arises to create new random groups HDUs with this library, you may use * {@link RandomGroupsHDU#createFrom(Object[][])} instead. *

* * @return a newly created HDU from the given data kernel. * * @param o The data to be described in this HDU. * * @throws FitsException if the parameter could not be converted to a HDU. * * @see RandomGroupsHDU#createFrom(Object[][]) */ public static BasicHDU makeHDU(Object o) throws FitsException { return FitsFactory.hduFactory(o); } /** * Returns the version sting of this FITS library * * @return the version of the library. */ public static String version() { Properties props = new Properties(); try (InputStream versionProperties = Fits.class .getResourceAsStream("/META-INF/maven/gov.nasa.gsfc.heasarc/nom-tam-fits/pom.properties")) { props.load(versionProperties); return props.getProperty("version"); } catch (IOException e) { LOG.log(Level.INFO, "reading version failed, ignoring", e); return "unknown"; } } /** * close the input stream, and ignore eventual errors. * * @deprecated Use try-with-resources constructs in Java 8+ instead. * * @param in the input stream to close. */ public static void saveClose(InputStream in) { SafeClose.close(in); } /** * Add an HDU to the Fits object. Users may intermix calls to functions which read HDUs from an associated input * stream with the addHDU and insertHDU calls, but should be careful to understand the consequences. * * @param myHDU The HDU to be added to the end of the FITS object. * * @throws FitsException if the HDU could not be inserted. * * @see #readHDU() */ public void addHDU(BasicHDU myHDU) throws FitsException { insertHDU(myHDU, getNumberOfHDUs()); } /** * Get the current number of HDUs in the Fits object. * * @return The number of HDU's in the object. * * @deprecated use {@link #getNumberOfHDUs()} instead */ @Deprecated public int currentSize() { return getNumberOfHDUs(); } /** * Delete an HDU from the HDU list. * * @param n The index of the HDU to be deleted. If n is 0 and there is more than one HDU present, then * the next HDU will be converted from an image to primary HDU if possible. If not a dummy * header HDU will then be inserted. * * @throws FitsException if the HDU could not be deleted. */ public void deleteHDU(int n) throws FitsException { int size = getNumberOfHDUs(); if (n < 0 || n >= size) { throw new FitsException("Attempt to delete non-existent HDU:" + n); } hduList.remove(n); if (n == 0 && size > 1) { BasicHDU newFirst = hduList.get(0); if (newFirst.canBePrimary()) { newFirst.setPrimaryHDU(true); } else { insertHDU(BasicHDU.getDummyHDU(), 0); } } } /** * @deprecated Will be private in 2.0. Get a stream from the file and then use the stream * initialization. * * @param myFile The File to be associated. * @param compressed Is the data compressed? * * @throws FitsException if the opening of the file failed. */ // TODO Make private @Deprecated @SuppressWarnings("resource") protected void fileInit(File myFile, boolean compressed) throws FitsException { try { if (compressed) { streamInit(Files.newInputStream(myFile.toPath())); } else { randomInit(myFile); } } catch (IOException e) { throw new FitsException("Unable to create Input Stream from File: " + myFile, e); } } /** * Returns the n'th HDU. If the HDU is already read simply return a pointer to the cached data. Otherwise read the * associated stream until the n'th HDU is read. * * @param n The index of the HDU to be read. The primary HDU is index 0. * * @return The n'th HDU or null if it could not be found. * * @throws FitsException if the header could not be read * @throws IOException if the underlying buffer threw an error * @throws IndexOutOfBoundsException if the Fits contains no HDU by the given index. * * @see #getHDU(String) * @see #getHDU(String, int) */ public BasicHDU getHDU(int n) throws FitsException, IOException, IndexOutOfBoundsException { for (int i = getNumberOfHDUs(); i <= n; i++) { BasicHDU hdu = readHDU(); if (hdu == null) { return null; } } return hduList.get(n); } /** * Returns the primary header of this FITS file, that is the header of the primary HDU in this Fits object. This * method differs from getHDU(0).getHeader(), int that the primary header this way will be properly * configured as the primary HDU with all mandatory keywords, even if the HDU's header did not contain these entries * originally. (Subsequent calls to getHDU(0).getHeader() will also contain the populated mandatory * keywords). * * @return The primary header of this FITS file/object. * * @throws FitsException If the Fits is empty (does not contain a primary HDU) * @throws IOException if there was a problem accessing the FITS from the input * * @see #getCompleteHeader(int) * @see BasicHDU#getHeader() * * @since 1.19 */ public Header getPrimaryHeader() throws FitsException, IOException { if (hduList.isEmpty()) { throw new FitsException("Empty Fits object"); } BasicHDU primary = getHDU(0); primary.setPrimaryHDU(true); return primary.getHeader(); } /** * Returns the 'complete' header of the nth HDU in this FITS file/object. This differs from * {@link #getHDU(int)}.getHeader() in two important ways: *
    *
  • The header will be populated with the mandatory FITS keywords based on whether it is that of a primary or * extension HDU in this Fits, and the type of HDU it is. (Subsequent calls to getHDU(n).getHeader() * will also include the populated mandatory keywords.)
  • *
  • If the header contains the {@link Standard#INHERIT} keyword, a new header object is returned, which merges * the non-conflicting primary header keys on top of the keywords explicitly defined in the HDU already. *
* * @param n The zero-based index of the HDU. * * @return The completed header of the HDU. If the HDU contains the INHERIT key this * header will be a new header object constructed by this call to include also * all non-conflicting primary header keywords. Otherwise it will simply * return the HDUs header (after adding the mandatory keywords). * * @throws FitsException If the FITS is empty * @throws IOException If the HDU is not accessible from its source * @throws IndexOutOfBoundsException If the FITS does not contain a HDU by the specified index * * @see #getCompleteHeader(String) * @see #getCompleteHeader(String, int) * @see #getPrimaryHeader() * @see #getHDU(int) * * @since 1.19 */ public Header getCompleteHeader(int n) throws FitsException, IOException, IndexOutOfBoundsException { BasicHDU hdu = getHDU(n); if (hdu == null) { throw new IndexOutOfBoundsException("FITS has no HDU index " + n); } return getCompleteHeader(hdu); } /** * Returns the complete header of the first HDU by the specified name in this FITS file/object. This differs from * {@link #getHDU(String)}.getHeader() in two important ways: *
    *
  • The header will be populated with the mandatory FITS keywords based on whether it is that of a primary or * extension HDU in this Fits, and the type of HDU it is. (Subsequent calls to getHDU(n).getHeader() * will also include the populated mandatory keywords.)
  • *
  • If the header contains the {@link Standard#INHERIT} keyword, a new header object is returned, which merges * the non-conflicting primary header keys on top of the keywords explicitly defined in the HDU already. *
* * @param name The HDU name * * @return The completed header of the HDU. If the HDU contains the INHERIT key this header * will be a new header object constructed by this call to include also all * non-conflicting primary header keywords. Otherwise it will simply return the * HDUs header (after adding the mandatory keywords). * * @throws FitsException If the FITS is empty * @throws IOException If the HDU is not accessible from its source * @throws NoSuchElementException If the FITS does not contain a HDU by the specified name * * @see #getCompleteHeader(String, int) * @see #getCompleteHeader(int) * @see #getPrimaryHeader() * @see #getHDU(int) * * @since 1.19 */ public Header getCompleteHeader(String name) throws FitsException, IOException, NoSuchElementException { BasicHDU hdu = getHDU(name); if (hdu == null) { throw new NoSuchElementException("Fits contains no HDU named " + name); } return getCompleteHeader(hdu); } /** * Returns the complete header of the first HDU by the specified name and version in this FITS file/object. This * differs from {@link #getHDU(String)}.getHeader() in two important ways: *
    *
  • The header will be populated with the mandatory FITS keywords based on whether it is that of a primary or * extension HDU in this Fits, and the type of HDU it is. (Subsequent calls to getHDU(n).getHeader() * will also include the populated mandatory keywords.)
  • *
  • If the header contains the {@link Standard#INHERIT} keyword, a new header object is returned, which merges * the non-conflicting primary header keys on top of the keywords explicitly defined in the HDU already. *
* * @param name The HDU name * @param version The HDU version * * @return The completed header of the HDU. If the HDU contains the INHERIT key this header * will be a new header object constructed by this call to include also all * non-conflicting primary header keywords. Otherwise it will simply return the * HDUs header (after adding the mandatory keywords). * * @throws FitsException If the FITS is empty * @throws IOException If the HDU is not accessible from its source * @throws NoSuchElementException If the FITS does not contain a HDU by the specified name and version * * @see #getCompleteHeader(String) * @see #getCompleteHeader(int) * @see #getPrimaryHeader() * @see #getHDU(int) * * @since 1.19 */ public Header getCompleteHeader(String name, int version) throws FitsException, IOException, NoSuchElementException { BasicHDU hdu = getHDU(name, version); if (hdu == null) { throw new NoSuchElementException("Fits contains no HDU named " + name); } return getCompleteHeader(hdu); } private Header getCompleteHeader(BasicHDU hdu) throws FitsException, IOException { if (hdu == getHDU(0)) { return getPrimaryHeader(); } hdu.setPrimaryHDU(false); Header h = hdu.getHeader(); if (h.getBooleanValue(Standard.INHERIT)) { Header merged = new Header(); merged.mergeDistinct(h); merged.mergeDistinct(getPrimaryHeader()); return merged; } return h; } /** * Checks if the value of the EXTNAME keyword of the specified HDU matches the specified name. * * @param hdu The HDU whose EXTNAME to check * @param name The expected name * * @return true if the HDU has an EXTNAME keyword whose value matches the specified name (case * sensitive!), otherwise false * * @see #getHDU(String) */ private boolean isNameMatch(BasicHDU hdu, String name) { Header h = hdu.getHeader(); if (!h.containsKey(EXTNAME)) { return false; } return name.equals(hdu.getHeader().getStringValue(EXTNAME)); } /** * Checks if the value of the EXTNAME and EXTVER keywords of the specified HDU match the specified name and version. * * @param hdu The HDU whose EXTNAME to check * @param name The expected name * @param version The expected extension version * * @return true if the HDU has an EXTNAME keyword whose value matches the specified name (case * sensitive!) AND has an EXTVER keyword whose value matches the specified integer version. In * all other cases false is returned. * * @see #getHDU(String, int) */ private boolean isNameVersionMatch(BasicHDU hdu, String name, int version) { Header h = hdu.getHeader(); if (!h.containsKey(EXTNAME) || !name.equals(h.getStringValue(EXTNAME)) || !h.containsKey(EXTVER)) { return false; } return h.getIntValue(EXTVER) == version; } /** * Returns the HDU by the given extension name (defined by EXTNAME header keyword). This method checks * only for EXTNAME but will ignore the version (defined by EXTVER). If multiple HDUs have the same * matching EXTNAME, this method will return the first match only. * * @param name The name of the HDU as defined by EXTNAME (case sensitive) * * @return The first HDU that matches the specified extension name and version, or null * if the FITS does not contain a matching HDU. * * @throws FitsException if the header could not be read * @throws IOException if the underlying buffer threw an error * * @since 1.17.0 * * @see #getHDU(String, int) * @see #getHDU(int) */ public BasicHDU getHDU(String name) throws FitsException, IOException { // Check HDUs we already read... for (BasicHDU hdu : hduList) { if (isNameMatch(hdu, name)) { return hdu; } } // Read additional HDUs as necessary... BasicHDU hdu; while ((hdu = readHDU()) != null) { if (isNameMatch(hdu, name)) { return hdu; } } return null; } /** * Returns the HDU by the given extension name and version (defined by EXTNAME and EXTVER * keywords). If multiple HDUs have the same matching name and version, this method will return the first match * only. * * @param name The name of the HDU as defined by EXTNAME (case sensitive) * @param version The extension version as defined by EXTVER in the matching HDU. * * @return The first HDU that matches the specified extension name and version, or null * if the FITS does not contain a matching HDU. * * @throws FitsException if the header could not be read * @throws IOException if the underlying buffer threw an error * * @since 1.17.0 * * @see #getHDU(String) * @see #getHDU(int) */ public BasicHDU getHDU(String name, int version) throws FitsException, IOException { // Check HDUs we already read... for (BasicHDU hdu : hduList) { if (isNameVersionMatch(hdu, name, version)) { return hdu; } } // Read additional HDUs as necessary... BasicHDU hdu; while ((hdu = readHDU()) != null) { if (isNameVersionMatch(hdu, name, version)) { return hdu; } } return null; } /** * Get the number of HDUs currently available in memory. For FITS objects associated with an input this method * returns only the number of HDUs that have already been read / scanned, e.g. via {@link #readHDU()} or * {@link #read()} methods. Thus, if you want to know how many HDUs a FITS file might actually contain, you should * call {@link #read()} to register them all before calling this method. * * @return The number of HDU's in the object. * * @see #read() * @see #readHDU() */ public int getNumberOfHDUs() { return hduList.size(); } /** * Returns the input from which this Fits is associated to (if any).. * * @return The associated data input, or null if this Fits container was not read from an * input. Users may wish to call this function after opening a Fits object when they want low-level * rea/wrte access to the FITS resource directly. */ public ArrayDataInput getStream() { return dataStr; } /** * Insert a FITS object into the list of HDUs. * * @param myHDU The HDU to be inserted into the list of HDUs. * @param position The location at which the HDU is to be inserted. * * @throws FitsException if the HDU could not be inserted. */ public void insertHDU(BasicHDU myHDU, int position) throws FitsException { if (myHDU == null) { return; } if (position < 0 || position > getNumberOfHDUs()) { throw new FitsException("Attempt to insert HDU at invalid location: " + position); } if (myHDU instanceof RandomGroupsHDU && position != 0) { throw new FitsException("Random groups HDUs must be the first (primary) HDU. Requested pos: " + position); } try { if (position == 0) { // Note that the previous initial HDU is no longer the first. // If we were to insert tables backwards from last to first, // we could get a lot of extraneous DummyHDUs but we currently // do not worry about that. if (getNumberOfHDUs() > 0) { hduList.get(0).setPrimaryHDU(false); } if (myHDU.canBePrimary()) { myHDU.setPrimaryHDU(true); hduList.add(0, myHDU); } else { insertHDU(BasicHDU.getDummyHDU(), 0); myHDU.setPrimaryHDU(false); hduList.add(1, myHDU); } } else { myHDU.setPrimaryHDU(false); hduList.add(position, myHDU); } } catch (NoSuchElementException e) { throw new FitsException("hduList inconsistency in insertHDU", e); } } /** * Initialize using buffered random access. This implies that the data is uncompressed. * * @param file the file to open * * @throws FitsException if the file could not be read * * @see #randomInit(RandomAccessFileIO) */ // TODO make private @Deprecated protected void randomInit(File file) throws FitsException { if (!file.exists() || !file.canRead()) { throw new FitsException("Non-existent or unreadable file"); } try { // Attempt to open the file for reading and writing. dataStr = new FitsFile(file, "rw"); ((FitsFile) dataStr).seek(0); } catch (IOException e) { try { // If that fails, try read-only. dataStr = new FitsFile(file, "r"); ((FitsFile) dataStr).seek(0); } catch (IOException e2) { throw new FitsException("Unable to open file " + file.getPath(), e2); } } } /** * Initialize using buffered random access. This implies that the data is uncompressed. * * @param src the random access data * * @throws FitsException ` if the data is not readable * * @see #randomInit(File) */ protected void randomInit(RandomAccessFileIO src) throws FitsException { try { dataStr = new FitsFile(src, FitsIO.DEFAULT_BUFFER_SIZE); ((FitsFile) dataStr).seek(0); } catch (IOException e) { throw new FitsException("Unable to open data " + src, e); } } /** * Return all HDUs for the Fits object. If the FITS file is associated with an external stream make sure that we * have exhausted the stream. * * @return an array of all HDUs in the Fits object. Returns null if there are no HDUs associated with * this object. * * @throws FitsException if the reading failed. */ public BasicHDU[] read() throws FitsException { readToEnd(); int size = getNumberOfHDUs(); if (size == 0) { return new BasicHDU[0]; } return hduList.toArray(new BasicHDU[size]); } /** * Read a FITS file from an InputStream object. * * @param is The InputStream stream whence the FITS information is found. * * @throws FitsException if the data read could not be interpreted * * @deprecated Use {@link #Fits(InputStream)} constructor instead. We will remove this method in the * future. */ public void read(InputStream is) throws FitsException { is = CompressionManager.decompress(is); if (is instanceof ArrayDataInput) { dataStr = (ArrayDataInput) is; } else { dataStr = new FitsInputStream(is); } read(); } /** * Read the next HDU on the default input stream. This call may return any concrete subclass of {@link BasicHDU}, * including compressed HDU types. * * @return The HDU read, or null if an EOF was detected. Note that null is only returned when the EOF * is detected immediately at the beginning of reading the HDU. * * @throws FitsException if the header could not be read * @throws IOException if the underlying buffer threw an error * * @see #skipHDU() * @see #getHDU(int) * @see #addHDU(BasicHDU) */ public BasicHDU readHDU() throws FitsException, IOException { if (dataStr == null || atEOF) { if (dataStr == null) { LOG.warning("trying to read a hdu, without an input source!"); } return null; } if (dataStr instanceof RandomAccess && lastFileOffset > 0) { FitsUtil.reposition(dataStr, lastFileOffset); } Header hdr = Header.readHeader(dataStr); if (hdr == null) { atEOF = true; return null; } Data data = FitsFactory.dataFactory(hdr); try { data.read(dataStr); if (Fits.checkTruncated(dataStr)) { // Check for truncation even if we successfully skipped to the expected // end since skip may allow going beyond the EOF. LOG.warning("Missing padding after data segment"); } } catch (PaddingException e) { // Stream end before required padding after data... LOG.warning(e.getMessage()); } lastFileOffset = FitsUtil.findOffset(dataStr); BasicHDU hdu = FitsFactory.hduFactory(hdr, data); hduList.add(hdu); return hdu; } /** * Read to the end of the associated input stream * * @throws FitsException if the operation failed */ private void readToEnd() throws FitsException { try { while (dataStr != null && !atEOF) { if (readHDU() == null) { if (getNumberOfHDUs() == 0) { throw new FitsException("Not FITS file."); } return; } } } catch (IOException e) { throw new FitsException("Corrupted FITS file: " + e, e); } } /** *

* Computes the CHECKSUM and DATASUM values for the specified HDU index and stores them in * the HUS's header. For deferred data the data sum is calculated directly from the file (if possible), without * loading the entire (potentially huge) data into RAM for the calculation. *

* * @param hduIndex The index of the HDU for which to compute and set the CHECKSUM and * DATASUM header values. * * @throws FitsException if there was a problem computing the checksum for the HDU * @throws IOException if there was an I/O error while accessing the data from the input * * @see #setChecksum() * @see BasicHDU#verifyIntegrity() * @see BasicHDU#verifyDataIntegrity() * * @since 1.17 */ public void setChecksum(int hduIndex) throws FitsException, IOException { FitsCheckSum.setDatasum(getHDU(hduIndex).getHeader(), calcDatasum(hduIndex)); } /** *

* Add or modify the CHECKSUM keyword in all headers. As of 1.17 the checksum for deferred data is calculated * directly from the file (if possible), without loading the entire (potentially huge) data into RAM for the * calculation. *

*

* As of 1.17, the routine calculates checksums both for HDUs that are in RAM, as well as HDUs that were not yet * loaded from the input (if any). Any HDUs not in RAM at the time of the call will stay in deferred mode (if the * HDU itself supports it). After setting (new) checksums, you may want to call #rewrite() *

* * @throws FitsException if there was an error during the checksumming operation * @throws IOException if there was an I/O error while accessing the data from the input * * @author R J Mather, Attila Kovacs * * @see #setChecksum(int) * @see BasicHDU#getStoredDatasum() * @see #rewrite() */ public void setChecksum() throws FitsException, IOException { int i = 0; // Start with HDU's already loaded, leaving deferred data in unloaded // state for (; i < getNumberOfHDUs(); i++) { setChecksum(i); } // Check if Fits is read from an input of sorts, with potentially more // HDUs there... if (dataStr == null) { return; } // Continue with unread HDUs (if any...) while (readHDU() != null) { setChecksum(i++); } } /** *

* Calculates the data checksum for a given HDU in the Fits. If the HDU does not currently have data loaded from * disk (in deferred read mode), the method will calculate the checksum directly from disk. Otherwise, it will * calculate the datasum from the data in memory. *

* * @param hduIndex The index of the HDU for which to calculate the data checksum * * @return The data checksum. This may differ from the datasum or the original FITS input due to * differences in padding used at the end of the data record by this library vs the * library that was used to generate the FITS. * * @throws FitsException if there was an error processing the HDU. * @throws IOException if there was an I/O error accessing the input. * * @see Data#calcChecksum() * @see BasicHDU#verifyDataIntegrity() * @see #setChecksum(int) * @see BasicHDU#getStoredDatasum() * @see FitsCheckSum#setDatasum(Header, long) * * @since 1.17 */ public long calcDatasum(int hduIndex) throws FitsException, IOException { BasicHDU hdu = getHDU(hduIndex); Data data = hdu.getData(); if (data.isDeferred()) { // Compute datasum directly from file... return FitsCheckSum.checksum((RandomAccess) dataStr, data.getFileOffset(), data.getSize()); } return data.calcChecksum(); } /** * Calculates the FITS checksum for a given HDU in the Fits. If the HDU does not currently have data loaded from * disk (i.e. in deferred read mode), the method will compute the checksum directly from disk. Otherwise, it will * calculate the checksum from the data in memory and using the standard padding after it. * * @deprecated Use {@link BasicHDU#verifyIntegrity()} instead when appropriate. It's not particularly * useful since integrity checking does not use or require knowledge of this sum. May * be removed from future releases. * * @param hduIndex The index of the HDU for which to calculate the HDU checksum * * @return The checksum value that would appear in the header if this HDU was written to an * output. This may differ from the checksum recorded in the input, due to different * formating conventions used by this library vs the one that was used to generate the * input. * * @throws FitsException if there was an error processing the HDU. * @throws IOException if there was an I/O error accessing the input. * * @see BasicHDU#calcChecksum() * @see #calcDatasum(int) * @see #setChecksum(int) * * @since 1.17 */ public long calcChecksum(int hduIndex) throws FitsException, IOException { return FitsCheckSum.sumOf(FitsCheckSum.checksum(getHDU(hduIndex).getHeader()), calcDatasum(hduIndex)); } /** * Checks the integrity of all HDUs. HDUs that do not specify either CHECKSUM or DATASUM keyword will be ignored. * * @throws FitsIntegrityException if the FITS is corrupted, the message will inform about which HDU failed the * integrity test first. * @throws FitsException if the header or HDU is invalid or garbled. * @throws IOException if the Fits object is not associated to a random-accessible input, or if there was * an I/O error accessing the input. * * @see BasicHDU#verifyIntegrity() * * @since 1.18.1 */ public void verifyIntegrity() throws FitsIntegrityException, FitsException, IOException { for (int i = 0;; i++) { BasicHDU hdu = readHDU(); if (hdu == null) { break; } try { hdu.verifyIntegrity(); } catch (FitsIntegrityException e) { throw new FitsIntegrityException(i, e); } } } /** * @deprecated This method is poorly conceived as we cannot really read FITS from just any * ArrayDataInput but only those, which utilize {@link nom.tam.util.FitsDecoder} * to convert Java types to FITS binary format, such as {@link FitsInputStream} or * {@link FitsFile} (or else a wrapped DataInputStream). As such, this method is * inherently unsafe as it can be used to parse FITS content iscorrectly. It will be removed * from the public API in a future major release. Set the data stream to be used for future * input. * * @param stream The data stream to be used. */ @Deprecated public void setStream(ArrayDataInput stream) { dataStr = stream; atEOF = false; lastFileOffset = -1; } /** * Return the number of HDUs in the Fits object. If the FITS file is associated with an external stream make sure * that we have exhausted the stream. * * @return number of HDUs. * * @deprecated The meaning of size of ambiguous. Use {@link #getNumberOfHDUs()} instead. Note size() * will read the input file/stream to the EOF before returning the number of HDUs * which {@link #getNumberOfHDUs()} does not. If you wish to duplicate this behavior * and ensure that the input has been exhausted before getting the number of HDUs then * use the sequence: * read(); * getNumberOfHDUs(); * * * @throws FitsException if the file could not be read. */ @Deprecated public int size() throws FitsException { readToEnd(); return getNumberOfHDUs(); } /** * Skip the next HDU on the default input stream. * * @throws FitsException if the HDU could not be skipped * @throws IOException if the underlying stream failed * * @see #skipHDU(int) * @see #readHDU() */ public void skipHDU() throws FitsException, IOException { if (atEOF) { return; } Header hdr = new Header(dataStr); int dataSize = (int) hdr.getDataSize(); dataStr.skipAllBytes(dataSize); if (dataStr instanceof RandomAccess) { lastFileOffset = ((RandomAccess) dataStr).getFilePointer(); } } /** * Skip HDUs on the associate input stream. * * @param n The number of HDUs to be skipped. * * @throws FitsException if the HDU could not be skipped * @throws IOException if the underlying stream failed * * @see #skipHDU() */ public void skipHDU(int n) throws FitsException, IOException { for (int i = 0; i < n; i++) { skipHDU(); } } /** * Initializes the input stream. Mostly this checks to see if the stream is compressed and wraps the stream if * necessary. Even if the stream is not compressed, it will likely be wrapped in a PushbackInputStream. So users * should probably not supply a BufferedDataInputStream themselves, but should allow the Fits class to do the * wrapping. * * @param inputStream stream to initialize * * @throws FitsException if the initialization failed */ @SuppressWarnings("resource") protected void streamInit(InputStream inputStream) throws FitsException { dataStr = new FitsInputStream(CompressionManager.decompress(inputStream)); } /** * Writes the contents to a designated FITS file. It is up to the caller to close the file as appropriate after * writing to it. * * @param file a file that support FITS encoding * * @throws FitsException if there were any errors writing the contents themselves. * @throws IOException if the underlying file could not be trimmed or closed. * * @since 1.16 * * @see #write(FitsOutputStream) */ public void write(FitsFile file) throws IOException, FitsException { write((ArrayDataOutput) file); file.setLength(file.getFilePointer()); } /** * Writes the contents to a designated FITS output stream. It is up to the caller to close the stream as appropriate * after writing to it. * * @param out an output stream that supports FITS encoding. * * @throws FitsException if there were any errors writing the contents themselves. * @throws IOException if the underlying file could not be flushed or closed. * * @since 1.16 * * @see #write(FitsFile) * @see #write(File) * @see #write(String) */ public void write(FitsOutputStream out) throws IOException, FitsException { write((ArrayDataOutput) out); out.flush(); } /** * Writes the contents to a new file. * * @param file a file to which the FITS is to be written. * * @throws FitsException if there were any errors writing the contents themselves. * @throws IOException if the underlying output stream could not be created or closed. * * @see #write(FitsOutputStream) */ public void write(File file) throws IOException, FitsException { try (FileOutputStream o = new FileOutputStream(file)) { write(new FitsOutputStream(o)); o.flush(); } } /** * Re-writes all HDUs that have been loaded (and possibly modified) to the disk, if possible -- or else does * nothing. For HDUs that are in deferred mode (data unloaded and unchanged), only the header is re-written to disk. * Otherwise, both header and data is re-written. Of course, rewriting is possible only if the sizes of all headers * and data segments remain the same as before. * * @throws FitsException If one or more of the HDUs cannot be re-written, or if there was some other error * serializing the HDUs to disk. * @throws IOException If there was an I/O error accessing the output file. * * @since 1.17 * * @see BasicHDU#rewriteable() */ public void rewrite() throws FitsException, IOException { for (int i = 0; i < getNumberOfHDUs(); i++) { if (!getHDU(i).rewriteable()) { throw new FitsException("HDU[" + i + "] cannot be re-written in place. Aborting rewrite."); } } for (int i = 0; i < getNumberOfHDUs(); i++) { getHDU(i).rewrite(); } } /** * Writes the contents to the specified file. It simply wraps {@link #write(File)} for convenience. * * @param fileName the file name/path * * @throws FitsException if there were any errors writing the contents themselves. * @throws IOException if the underlying stream could not be created or closed. * * @since 1.16 * * @see #write(File) */ public void write(String fileName) throws IOException, FitsException { write(new File(fileName)); } // TODO For DataOutputStream this one conflicts with write(DataOutput). // However // once that one is deprecated, this one can be exposed safely. // public void write(OutputStream os) throws IOException, FitsException { // write(new FitsOutputStream(os)); // } /** * Writes the contents to the specified output. This should not be exposed outside of this class, since the output * object must have FITS-specific encoding, and we can only make sure of that if this is called locally only. * * @param out the output with a FITS-specific encoding. * * @throws FitsException if the operation failed */ private void write(ArrayDataOutput out) throws FitsException { for (BasicHDU basicHDU : hduList) { basicHDU.write(out); } } /** * @deprecated This method is poorly conceived as we cannot really write FITS to just any * DataOutput but only to specific {@link ArrayDataOutput}, which utilize * {@link nom.tam.util.FitsEncoder} to convert Java types to FITS binary format, such * as {@link FitsOutputStream} or {@link FitsFile} (or else a wrapped * DataOutputStream). As such, this method is inherently unsafe as it can * be used to create unreadable FITS files. It will be removed from a future major * release. Use one of the more appropriate other write() methods * instead. Writes the contents to an external file or stream. The file or stream * remains open and it is up to the caller to close it as appropriate. * * @param os A DataOutput stream. * * @throws FitsException if the operation failed * * @see #write(FitsFile) * @see #write(FitsOutputStream) * @see #write(File) * @see #write(String) */ @Deprecated public void write(DataOutput os) throws FitsException { if (os instanceof FitsFile) { try { write((FitsFile) os); } catch (IOException e) { throw new FitsException("Error writing to FITS file: " + e, e); } return; } if (os instanceof FitsOutputStream) { try { write((FitsOutputStream) os); } catch (IOException e) { throw new FitsException("Error writing to FITS output stream: " + e, e); } return; } if (!(os instanceof DataOutputStream)) { throw new FitsException("Cannot create FitsOutputStream from class " + os.getClass().getName()); } try { write(new FitsOutputStream((DataOutputStream) os)); } catch (IOException e) { throw new FitsException("Error writing to the FITS output stream: " + e, e); } } @Override public void close() throws IOException { if (dataStr != null) { dataStr.close(); } } /** * set the checksum of a HDU. * * @param hdu the HDU to add a checksum * * @throws FitsException the checksum could not be added to the header * * @deprecated use {@link FitsCheckSum#setChecksum(BasicHDU)} */ @Deprecated public static void setChecksum(BasicHDU hdu) throws FitsException { FitsCheckSum.setChecksum(hdu); } /** * calculate the checksum for the block of data * * @param data the data to create the checksum for * * @return the checksum * * @deprecated use {@link FitsCheckSum#checksum(byte[])} */ @Deprecated public static long checksum(final byte[] data) { return FitsCheckSum.checksum(data); } /** * Checks if the file ends before the current read positon, and if so, it may log a warning. This may happen with * {@link FitsFile} where the contract of {@link RandomAccess} allows for skipping ahead beyond the end of file, * since expanding the file is allowed when writing. Only a subsequent read call would fail. * * @param in the input from which the FITS content was read. * * @return true if the current read position is beyond the end-of-file, otherwise * false. * * @throws IOException if there was an IO error accessing the file or stream. * * @see ArrayDataInput#skip(long) * @see ArrayDataInput#skipBytes(int) * @see ArrayDataInput#skipAllBytes(long) * * @since 1.16 */ static boolean checkTruncated(ArrayDataInput in) throws IOException { if (!(in instanceof RandomAccess)) { // We cannot skip more than is available in an input stream. return false; } RandomAccess f = (RandomAccess) in; long pos = f.getFilePointer(); long len = f.length(); if (pos > len) { LOG.log(Level.WARNING, "Premature file end at " + len + " (expected " + pos + ")", new Throwable()); return true; } return false; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/FitsDate.java000066400000000000000000000262451476377620500234610ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.text.DecimalFormat; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ISO timestamp support for FITS headers. Such timestamps are used with DATE style header keywords, such * as DATE-OBS or DATE-END. */ public class FitsDate implements Comparable { /** * logger to log to. */ private static final int FIRST_THREE_CHARACTER_VALUE = 100; private static final int FIRST_TWO_CHARACTER_VALUE = 10; private static final int FITS_DATE_STRING_SIZE = 23; private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); private static final int NEW_FORMAT_DAY_OF_MONTH_GROUP = 4; private static final int NEW_FORMAT_HOUR_GROUP = 6; private static final int NEW_FORMAT_MILLISECOND_GROUP = 10; private static final int NEW_FORMAT_MINUTE_GROUP = 7; private static final int NEW_FORMAT_MONTH_GROUP = 3; private static final int NEW_FORMAT_SECOND_GROUP = 8; private static final int NEW_FORMAT_YEAR_GROUP = 2; private static final Pattern NORMAL_REGEX = Pattern.compile( "\\s*(([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9]))(T([0-9][0-9]):([0-9][0-9]):([0-9][0-9])(\\.([0-9]+))?)?\\s*"); private static final int OLD_FORMAT_DAY_OF_MONTH_GROUP = 1; private static final int OLD_FORMAT_MONTH_GROUP = 2; private static final int OLD_FORMAT_YEAR_GROUP = 3; private static final Pattern OLD_REGEX = Pattern.compile("\\s*([0-9][0-9])/([0-9][0-9])/([0-9][0-9])\\s*"); private static final int YEAR_OFFSET = 1900; private static final int NB_DIGITS_MILLIS = 3; private static final int POW_TEN = 10; /** * Returns the FITS date string for the current date and time. * * @return the current date in FITS date format * * @see #getFitsDateString(Date) */ public static String getFitsDateString() { return getFitsDateString(new Date(), true); } /** * Returns the FITS date string for a specific date and time * * @return a created FITS format date string Java Date object. * * @param epoch The epoch to be converted to FITS format. * * @see #getFitsDateString(Date, boolean) * @see #getFitsDateString() */ public static String getFitsDateString(Date epoch) { return getFitsDateString(epoch, true); } /** * Returns the FITS date string, with or without the time component, for a specific date and time. * * @return a created FITS format date string. Note that the date is not rounded. * * @param epoch The epoch to be converted to FITS format. * @param timeOfDay Whether the time of day information shouldd be included * * @see #getFitsDateString(Date) * @see #getFitsDateString() */ public static String getFitsDateString(Date epoch, boolean timeOfDay) { Calendar cal = Calendar.getInstance(UTC); cal.setTime(epoch); StringBuilder fitsDate = new StringBuilder(); DecimalFormat df = new DecimalFormat("0000"); fitsDate.append(df.format(cal.get(Calendar.YEAR))); fitsDate.append("-"); df = new DecimalFormat("00"); fitsDate.append(df.format(cal.get(Calendar.MONTH) + 1)); fitsDate.append("-"); fitsDate.append(df.format(cal.get(Calendar.DAY_OF_MONTH))); if (timeOfDay) { fitsDate.append("T"); fitsDate.append(df.format(cal.get(Calendar.HOUR_OF_DAY))); fitsDate.append(":"); fitsDate.append(df.format(cal.get(Calendar.MINUTE))); fitsDate.append(":"); fitsDate.append(df.format(cal.get(Calendar.SECOND))); fitsDate.append("."); df = new DecimalFormat("000"); fitsDate.append(df.format(cal.get(Calendar.MILLISECOND))); } return fitsDate.toString(); } private int hour = -1; private int mday = -1; private int millisecond = -1; private int minute = -1; private int month = -1; private int second = -1; private int year = -1; /** * Convert a FITS date string to a Java Date object. * * @param dStr the FITS date * * @throws FitsException if dStr does not contain a valid FITS date. */ public FitsDate(String dStr) throws FitsException { // if the date string is null, we are done if (dStr == null || dStr.isEmpty()) { return; } Matcher match = FitsDate.NORMAL_REGEX.matcher(dStr); if (match.matches()) { year = getInt(match, FitsDate.NEW_FORMAT_YEAR_GROUP); month = getInt(match, FitsDate.NEW_FORMAT_MONTH_GROUP); mday = getInt(match, FitsDate.NEW_FORMAT_DAY_OF_MONTH_GROUP); hour = getInt(match, FitsDate.NEW_FORMAT_HOUR_GROUP); minute = getInt(match, FitsDate.NEW_FORMAT_MINUTE_GROUP); second = getInt(match, FitsDate.NEW_FORMAT_SECOND_GROUP); millisecond = getMilliseconds(match, FitsDate.NEW_FORMAT_MILLISECOND_GROUP); } else { match = FitsDate.OLD_REGEX.matcher(dStr); if (!match.matches()) { if (dStr.trim().isEmpty()) { return; } throw new FitsException("Bad FITS date string \"" + dStr + '"'); } year = getInt(match, FitsDate.OLD_FORMAT_YEAR_GROUP) + FitsDate.YEAR_OFFSET; month = getInt(match, FitsDate.OLD_FORMAT_MONTH_GROUP); mday = getInt(match, FitsDate.OLD_FORMAT_DAY_OF_MONTH_GROUP); } } private static int getInt(Matcher match, int groupIndex) { String value = match.group(groupIndex); if (value != null) { return Integer.parseInt(value); } return -1; } private static int getMilliseconds(Matcher match, int groupIndex) { String value = match.group(groupIndex); if (value != null) { value = String.format("%-3s", value).replace(' ', '0'); int num = Integer.parseInt(value); if (value.length() > NB_DIGITS_MILLIS) { num = (int) Math.round(num / Math.pow(POW_TEN, value.length() - NB_DIGITS_MILLIS)); } return num; } return -1; } /** * Get a Java Date object corresponding to this FITS date. * * @return The Java Date object. */ public Date toDate() { if (year == -1) { return null; } Calendar cal = Calendar.getInstance(UTC); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, month - 1); cal.set(Calendar.DAY_OF_MONTH, mday); if (hour == -1) { cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); } else { cal.set(Calendar.HOUR_OF_DAY, hour); cal.set(Calendar.MINUTE, minute); cal.set(Calendar.SECOND, second); if (millisecond == -1) { cal.set(Calendar.MILLISECOND, 0); } else { cal.set(Calendar.MILLISECOND, millisecond); } } return cal.getTime(); } @Override public String toString() { if (year == -1) { return ""; } StringBuilder buf = new StringBuilder(FitsDate.FITS_DATE_STRING_SIZE); buf.append(year); buf.append('-'); appendTwoDigitValue(buf, month); buf.append('-'); appendTwoDigitValue(buf, mday); if (hour != -1) { buf.append('T'); appendTwoDigitValue(buf, hour); buf.append(':'); appendTwoDigitValue(buf, minute); buf.append(':'); appendTwoDigitValue(buf, second); if (millisecond != -1) { buf.append('.'); appendThreeDigitValue(buf, millisecond); } } return buf.toString(); } @Override public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof FitsDate)) { return false; } return compareTo((FitsDate) o) == 0; } @Override public int hashCode() { return Integer.hashCode(year) ^ Integer.hashCode(month) ^ Integer.hashCode(mday) ^ Integer.hashCode(hour) ^ Integer.hashCode(minute) ^ Integer.hashCode(second) ^ Integer.hashCode(millisecond); } @Override public int compareTo(FitsDate fitsDate) { int result = Integer.compare(year, fitsDate.year); if (result != 0) { return result; } result = Integer.compare(month, fitsDate.month); if (result != 0) { return result; } result = Integer.compare(mday, fitsDate.mday); if (result != 0) { return result; } result = Integer.compare(hour, fitsDate.hour); if (result != 0) { return result; } result = Integer.compare(minute, fitsDate.minute); if (result != 0) { return result; } result = Integer.compare(second, fitsDate.second); if (result != 0) { return result; } return Integer.compare(millisecond, fitsDate.millisecond); } private void appendThreeDigitValue(StringBuilder buf, int value) { if (value < FitsDate.FIRST_THREE_CHARACTER_VALUE) { buf.append('0'); } appendTwoDigitValue(buf, value); } private void appendTwoDigitValue(StringBuilder buf, int value) { if (value < FitsDate.FIRST_TWO_CHARACTER_VALUE) { buf.append('0'); } buf.append(value); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/FitsElement.java000066400000000000000000000100301476377620500241560ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; /** * I/O interface for various FITS file elements. */ public interface FitsElement { /** * Returns the byte offset at which this element starts ina file. If the * element was not obtained from an input, then 0 is returned. * * @return the byte at which this element begins. This is only available if * the data is originally read from a random access medium. * Otherwise 0 is returned. */ long getFileOffset(); /** * Returns the size of this elements in the FITS representation. This may * include padding if the element (such as a header or data segment) is * expected to complete a FITS block of 2880 bytes. * * @return The size of this element in bytes, or 0 if the element is empty * or invalid. */ long getSize(); /** * Read a FITS element from the input, starting at the current position. * Ater the read, the implementations should leave the input position * aligned to the start of the next FITS block. * * @param in * The input data stream * @throws FitsException * if the read was unsuccessful. * @throws IOException * if the read was unsuccessful. */ void read(ArrayDataInput in) throws FitsException, IOException; /** * Reset the input stream to point to the beginning of this element * * @return True if the reset succeeded. */ boolean reset(); /** * Rewrite the contents of the element in place. The data must have been * originally read from a random access device, and the size of the element * may not have changed. * * @throws FitsException * if the rewrite was unsuccessful. * @throws IOException * if the rewrite was unsuccessful. */ void rewrite() throws FitsException, IOException; /** * Checks if we can write this element back to its source. An element can * only be written back if it is associated to a random accessible input and * the current size FITS within the old block size. * * @return true if this element can be rewritten? */ boolean rewriteable(); /** * Writes the contents of the element to a data sink, adding padding as * necessary if the element (such as a header or data segment) is expected * to complete the FITS block of 2880 bytes. * * @param out * The data sink. * @throws FitsException * if the write was unsuccessful. */ void write(ArrayDataOutput out) throws FitsException; } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/FitsException.java000066400000000000000000000050521476377620500245330ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * When we cannot deal with some FITS data as expected. Originally it was a hard * exception, that you had no choice by to catch. Since 1.19, it has been * demoted to a softer, runtime exception. This is a back compatible change, * which gives more freedom to programmers on dealing with these (or not). */ public class FitsException extends IllegalStateException { /** * */ private static final long serialVersionUID = 7713834647104490578L; /** * Instantiates this exception with the designated message string. * * @param msg * a human readable message that describes what in fact caused * the exception */ public FitsException(String msg) { super(msg); } /** * Instantiates this exception with the designated message string, when it * was triggered by some other type of exception * * @param msg * a human readable message that describes what in fact caused * the exception * @param reason * the original exception (or other throwable) that triggered * this exception. */ public FitsException(String msg, Throwable reason) { super(msg, reason); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/FitsFactory.java000066400000000000000000001007751476377620500242140ustar00rootroot00000000000000package nom.tam.fits; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import nom.tam.fits.header.Standard; import nom.tam.fits.header.hierarch.IHierarchKeyFormatter; import nom.tam.fits.header.hierarch.StandardIHierarchKeyFormatter; import nom.tam.image.compression.hdu.CompressedImageData; import nom.tam.image.compression.hdu.CompressedImageHDU; import nom.tam.image.compression.hdu.CompressedTableData; import nom.tam.image.compression.hdu.CompressedTableHDU; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * Controls the creation of HDUs to encapsulate a variery of data, based on a few configuration switches. The switches * allow for toggling support for different conventions to set the desired compatibility level. The default settings * produce FITS that are compatibel with version 4.0 of the standard (the latest at the time of writing this). The * switches may also be used to make this library more backward compatible with its previous version also. */ public final class FitsFactory { private static final boolean DEFAULT_USE_ASCII_TABLES = false; private static final boolean DEFAULT_USE_HIERARCH = true; private static final boolean DEFAULT_USE_EXPONENT_D = false; private static final boolean DEFAULT_LONG_STRINGS_ENABLED = true; private static final boolean DEFAULT_CHECK_ASCII_STRINGS = false; private static final boolean DEFAULT_ALLOW_TERMINAL_JUNK = true; private static final boolean DEFAULT_ALLOW_HEADER_REPAIRS = true; private static final boolean DEFAULT_SKIP_BLANK_AFTER_ASSIGN = false; private static final boolean DEFAULT_CASE_SENSITIVE_HIERARCH = false; /** * AK: true is the legacy behavior TODO If and when it is changed to false, the corresponding Logger warnings in * BinaryTable should also be removed. */ private static final boolean DEFAULT_USE_UNICODE_CHARS = true; private static final IHierarchKeyFormatter DEFAULT_HIERARCH_FORMATTER = new StandardIHierarchKeyFormatter(); /** * An class for aggregating all the settings internal to {@link FitsFactory}. * * @author Attila Kovacs */ protected static final class FitsSettings implements Cloneable { private boolean useAsciiTables; private boolean useHierarch; private boolean useExponentD; private boolean checkAsciiStrings; private boolean allowTerminalJunk; private boolean allowHeaderRepairs; private boolean longStringsEnabled; private boolean useUnicodeChars; @Deprecated private boolean skipBlankAfterAssign; private IHierarchKeyFormatter hierarchKeyFormatter = DEFAULT_HIERARCH_FORMATTER; private FitsSettings() { useAsciiTables = DEFAULT_USE_ASCII_TABLES; useHierarch = DEFAULT_USE_HIERARCH; useUnicodeChars = DEFAULT_USE_UNICODE_CHARS; checkAsciiStrings = DEFAULT_CHECK_ASCII_STRINGS; useExponentD = DEFAULT_USE_EXPONENT_D; allowTerminalJunk = DEFAULT_ALLOW_TERMINAL_JUNK; allowHeaderRepairs = DEFAULT_ALLOW_HEADER_REPAIRS; longStringsEnabled = DEFAULT_LONG_STRINGS_ENABLED; skipBlankAfterAssign = DEFAULT_SKIP_BLANK_AFTER_ASSIGN; hierarchKeyFormatter = DEFAULT_HIERARCH_FORMATTER; hierarchKeyFormatter.setCaseSensitive(DEFAULT_CASE_SENSITIVE_HIERARCH); } @Override protected FitsSettings clone() { try { return (FitsSettings) super.clone(); } catch (CloneNotSupportedException e) { return null; } } private FitsSettings copy() { return clone(); } /** * Returns the formatter instance for HIERARCH style keywords. Our own standard is to define such keywords * internally as starting with the string HIERARCH. followed by a dot-separated hierarchy, or just * an unusually long FITS keywords that cannot be represented by a standard 8-byte keyword. The HIERARCH * formatted will take such string keywords and will format them according to its rules when writing them to * FITS headers. * * @return The formatter instance used for HIERARCH-style keywords. */ protected IHierarchKeyFormatter getHierarchKeyFormatter() { return hierarchKeyFormatter; } /** * Checks if we should use the letter 'D' to mark exponents of double-precision values (in FITS headers and * ASCII tables). For ecample, in the typical Java number formatting the String 1.37E-13 may * represent either a float or double value -- which are not exactly the same. For * that reason FITS offers the possibility to replace 'E' in the string formatted number with 'D' when the value * specifies a double-precision number, thus disambiguating the two. * * @return true if we will use 'D' to denote the exponent of double-precision values in FITS * headers and ASCII tables. */ protected boolean isUseExponentD() { return useExponentD; } /** * Checks if we treat junk after the last properly formed HDU silently withotu generating an exception. When * this setting is true we can read corrupted FITS files (at least partially) without raising an * alarm. * * @return true if we allow additional bytes after the last readable HDU to be present in FITS * files without throwing an exception. Otherwise false. */ protected boolean isAllowTerminalJunk() { return allowTerminalJunk; } /** * Whether we check if ASCII strings in FITS files conform to the restricted set of characters (0x20 trough * 0x7E) allowed by the FITS standard. If the checking is enabled, we will log any such violations so they can * be inspected and perhaps fixed. * * @return true if we should check and report if string appearing in FITS files do not conform to * specification. Otherwise false */ protected boolean isCheckAsciiStrings() { return checkAsciiStrings; } /** * Checks if we allow storing long string values (using the OGIP 1.0 convention) in FITS headers. Such long * string may span multiple 80-character header records. They are now standard as of FITS 4.0, but they were not * in earlier specifications. When long strings are not enabled, we will throw a {@link LongValueException} * whenever one tries to add a string value that cannot be contained in a single 80-character header record. * * @return true (default) if we allow adding long string values to out FITS headers. Otherwise * false. */ protected boolean isLongStringsEnabled() { return longStringsEnabled; } /** * @deprecated The FITS standard is very explicit that assignment must be "= " (equals followed by a space). If * we allow skipping the space, it will result in a non-standard FITS, and may render it * unreadable for other tools. * * @return whether to use only "=", instead of the standard "= " between the keyword and the value. */ @Deprecated protected boolean isSkipBlankAfterAssign() { return skipBlankAfterAssign; } /** * Whether to write tables as ASCII tables automatically if possible. Binary tables are generally always a * better option, as they are both more compact and flexible but sometimes we might want to make our table data * to be human readable in a terminal without needing any FITS-specific tool -- even though the 1970s is long * past... * * @return true if we have a preference for writing table data in ASCII format (rather than * binary), whenever that is possible. Otherwise false */ protected boolean isUseAsciiTables() { return useAsciiTables; } /** * Whether we allow using HIERARCH-style keywords, which may be longer than the standard 8-character FITS * keywords, and may specify a hierarchy, and may also allow upper and lower-case characters depending on what * formatting rules we use. Our own standard is to define such keywords internally as starting with the string * HIERARCH. followed by a dot-separated hierarchy, or just an unusually long FITS keywords that * cannot be represented by a standard 8-byte keyword. * * @return true if we allow HIERARCH keywords. Otherwise false */ protected boolean isUseHierarch() { return useHierarch; } /** * Checks if we allow storing Java char[] arrays in binary tables as 16-bit short[]. * Otherwise we will store them as simple 8-bit ASCII. * * @return true if char[] is stored as short[] in binary tables, or * false if we store than as 8-bit ASCII. */ protected boolean isUseUnicodeChars() { return useUnicodeChars; } /** * Checks if we are tolerant to FITS standard violations when reading 3rd party FITS files. * * @return true if we tolerate minor violations of the FITS standard when interpreting headers, * which are unlikely to affect the integrity of the FITS otherwise. The violations will still be * logged, but no exception will be generated. Or, false if we want to generate * exceptions for such error.s */ protected boolean isAllowHeaderRepairs() { return allowHeaderRepairs; } } private static final FitsSettings GLOBAL_SETTINGS = new FitsSettings(); private static final ThreadLocal LOCAL_SETTINGS = new ThreadLocal<>(); private static ExecutorService threadPool; /** * the size of a FITS block in bytes. */ public static final int FITS_BLOCK_SIZE = 2880; /** * @deprecated (for internal use) Will reduce visibility in the future * * @return Given a Header construct an appropriate data. * * @param hdr header to create the data from * * @throws FitsException if the header did not contain enough information to detect the type of the data */ @Deprecated public static Data dataFactory(Header hdr) throws FitsException { if (ImageHDU.isHeader(hdr)) { if (hdr.getIntValue(Standard.NAXIS, 0) == 0) { return new NullData(); } Data d = ImageHDU.manufactureData(hdr); // Fix for positioning error noted by V. Forchi if (hdr.findCard(Standard.EXTEND) != null) { hdr.nextCard(); } return d; } if (RandomGroupsHDU.isHeader(hdr)) { return RandomGroupsHDU.manufactureData(hdr); } if (AsciiTableHDU.isHeader(hdr)) { return AsciiTableHDU.manufactureData(hdr); } if (CompressedImageHDU.isHeader(hdr)) { return CompressedImageHDU.manufactureData(hdr); } if (CompressedTableHDU.isHeader(hdr)) { return CompressedTableHDU.manufactureData(hdr); } if (BinaryTableHDU.isHeader(hdr)) { return BinaryTableHDU.manufactureData(hdr); } if (UndefinedHDU.isHeader(hdr)) { return UndefinedHDU.manufactureData(hdr); } throw new FitsException("Unrecognizable header in dataFactory"); } /** * Whether the letter 'D' may replace 'E' in the exponential notation of doubl-precision values. FITS allows (even * encourages) the use of 'D' to indicate double-recision values. For example to disambiguate between 1.37E-3 * (single-precision) and 1.37D-3 (double-precision), which are not exatly the same value in binary representation. * * @return Do we allow automatic header repairs, like missing end quotes? * * @since 1.16 * * @see #setUseExponentD(boolean) */ public static boolean isUseExponentD() { return current().isUseExponentD(); } /** * Whether char[] arrays are written as 16-bit integers (short[]) int binary tables as * opposed as FITS character arrays (byte[] with column type 'A'). See more explanation in * {@link #setUseUnicodeChars(boolean)}. * * @return true if char[] get written as 16-bit integers in binary table columns (column * type 'I'), or as FITS 1-byte ASCII character arrays (as is always the case for String) * with column type 'A'. * * @since 1.16 * * @see #setUseUnicodeChars(boolean) */ public static boolean isUseUnicodeChars() { return current().isUseUnicodeChars(); } /** * Whether extra bytes are tolerated after the end of an HDU. Normally if there is additional bytes present after an * HDU, it would be the beginning of another HDU -- which must start with a very specific sequence of bytes. So, * when there is data beyond the end of an HDU that does not appear to be another HDU, it's junk. We can either * ignore it, or throw an exception. * * @return Is terminal junk (i.e., non-FITS data following a valid HDU) allowed. * * @see #setAllowTerminalJunk(boolean) */ public static boolean getAllowTerminalJunk() { return current().isAllowTerminalJunk(); } /** * Whether we allow 3rd party FITS headers to be in violation of the standard, attempting to make sense of corrupted * header data as much as possible. * * @return Do we allow automatic header repairs, like missing end quotes? * * @see #setAllowHeaderRepairs(boolean) */ public static boolean isAllowHeaderRepairs() { return current().isAllowHeaderRepairs(); } /** * Returns the formatter instance for HIERARCH style keywords. Our own standard is to define such keywords * internally as starting with the string HIERARCH. followed by a dot-separated hierarchy, or just an * unusually long FITS keywords that cannot be represented by a standard 8-byte keyword. The HIERARCH formatted will * take such string keywords and will format them according to its rules when writing them to FITS headers. * * @return the formatter to use for hierarch keys. * * @see #setHierarchFormater(IHierarchKeyFormatter) */ public static IHierarchKeyFormatter getHierarchFormater() { return current().getHierarchKeyFormatter(); } /** * Whether we can use HIERARCH style keywords. Such keywords are not part of the current FITS standard, although * they constitute a recognised convention. Even if other programs may not process HIRARCH keywords themselves, * there is generally no harm to putting them into FITS headers, since the convention is such that these keywords * will be simply treated as comments by programs that do not recognise them. * * @return true if we are processing HIERARCH style keywords * * @see #setUseHierarch(boolean) */ public static boolean getUseHierarch() { return current().isUseHierarch(); } /** * whether ASCII tables should be used where feasible. * * @return true if we ASCII tables are allowed. * * @see #setUseAsciiTables(boolean) */ public static boolean getUseAsciiTables() { return current().isUseAsciiTables(); } /** * Checks whether we should check and validated ASCII strings that goe into FITS. FITS only allows ASCII characters * between 0x20 and 0x7E in ASCII tables. * * @return Get the current status for string checking. * * @see #setCheckAsciiStrings(boolean) */ public static boolean getCheckAsciiStrings() { return current().isCheckAsciiStrings(); } /** * Whether we allow storing long string in the header, which do not fit into a single 80-byte header record. Such * strings are then wrapped into multiple consecutive header records, OGIP 1.0 standard -- which is nart of FITS * 4.0, and was a recognised convention before. * * @return true If long string support is enabled. * * @see #setLongStringsEnabled(boolean) */ public static boolean isLongStringsEnabled() { return current().isLongStringsEnabled(); } /** * @return whether to use only "=", instead of the standard "= " between the keyword and the value. * * @deprecated The FITS standard is very explicit that assignment must be "= " (equals followed by a blank space). * If we allow skipping the space, it will result in a non-standard FITS, that is likely to break * compatibility with other tools. * * @see #setSkipBlankAfterAssign(boolean) */ @Deprecated public static boolean isSkipBlankAfterAssign() { return current().isSkipBlankAfterAssign(); } /** * . * * @deprecated (for internal use)/ Will reduce visibility in the future * * @return Given Header and data objects return the appropriate type of HDU. * * @param hdr the header, including a description of the data layout. * @param d the type of data object * @param the class of the data * * @throws FitsException if the operation failed */ @Deprecated @SuppressWarnings("unchecked") public static BasicHDU hduFactory(Header hdr, DataClass d) throws FitsException { if (d == null) { return (BasicHDU) new NullDataHDU(hdr); } if (d instanceof ImageData) { return (BasicHDU) new ImageHDU(hdr, (ImageData) d); } if (d instanceof CompressedImageData) { return (BasicHDU) new CompressedImageHDU(hdr, (CompressedImageData) d); } if (d instanceof RandomGroupsData) { return (BasicHDU) new RandomGroupsHDU(hdr, (RandomGroupsData) d); } if (d instanceof AsciiTable) { return (BasicHDU) new AsciiTableHDU(hdr, (AsciiTable) d); } if (d instanceof CompressedTableData) { return (BasicHDU) new CompressedTableHDU(hdr, (CompressedTableData) d); } if (d instanceof BinaryTable) { return (BasicHDU) new BinaryTableHDU(hdr, (BinaryTable) d); } if (d instanceof UndefinedData) { return (BasicHDU) new UndefinedHDU(hdr, (UndefinedData) d); } return null; } /** * Creates an HDU that wraps around the specified data object. The HDUs header will be created and populated with * the essential description of the data. The following HDU types may be returned depending on the nature of the * argument: *
    *
  • {@link NullDataHDU} -- if the argument is null
  • *
  • {@link ImageHDU} -- if the argument is a regular numerical array, such as a double[], * float[][], or short[][][]
  • *
  • {@link BinaryTableHDU} -- the the argument is an Object[rows][cols] type array with a regular * structure and supported column data types, provided that it cannot be represented by an ASCII table OR if * {@link FitsFactory#getUseAsciiTables()} is false
  • *
  • {@link AsciiTableHDU} -- Like above, but only when the data can be represented by an ASCII table AND * {@link FitsFactory#getUseAsciiTables()} is true
  • *
* * @return An appropriate HDU to encapsulate the given Java data object * * @param o The object to be described. * * @throws FitsException if the parameter could not be converted to a HDU because the binary representation of * the object is not known.. * * @deprecated Use {@link Fits#makeHDU(Object)} instead (this method may either be migrated to * {@link Fits} entirely or else have visibility reduced to the package level). */ public static BasicHDU hduFactory(Object o) throws FitsException { Data d; Header h; if (o == null) { return new NullDataHDU(); } else if (o instanceof Header) { h = (Header) o; d = dataFactory(h); } else if (ImageHDU.isData(o)) { d = ImageHDU.encapsulate(o); h = ImageHDU.manufactureHeader(d); } else if (current().isUseAsciiTables() && AsciiTableHDU.isData(o)) { d = AsciiTableHDU.encapsulate(o); h = AsciiTableHDU.manufactureHeader(d); } else if (BinaryTableHDU.isData(o)) { d = BinaryTableHDU.encapsulate(o); h = BinaryTableHDU.manufactureHeader(d); } else { throw new FitsException("This type of data is not supported for FITS representation"); } return hduFactory(h, d); } // CHECKSTYLE:OFF /** * @deprecated (duplicate method for internal use) Same as {@link #hduFactory(Header, Data)}, * and will be removed in the future. * * @return Given Header and data objects return the appropriate type of HDU. * * @param hdr the header of the date * @param d the data * @param the class of the data * * @throws FitsException if the operation failed */ @Deprecated public static BasicHDU HDUFactory(Header hdr, DataClass d) throws FitsException { return hduFactory(hdr, d); } // CHECKSTYLE:ON // CHECKSTYLE:OFF /** * @return Given an object, create the appropriate FITS header to describe it. * * @param o The object to be described. * * @throws FitsException if the parameter could not be converted to a hdu. * * @deprecated Use {@link Fits#makeHDU(Object)} instead (will removed in the future. Duplicate of * {@link #hduFactory(Object)} */ @Deprecated public static BasicHDU HDUFactory(Object o) throws FitsException { return hduFactory(o); } // CHECKSTYLE:ON /** * Restores all settings to their default values. * * @since 1.16 */ public static void setDefaults() { FitsSettings s = current(); s.useExponentD = DEFAULT_USE_EXPONENT_D; s.allowHeaderRepairs = DEFAULT_ALLOW_HEADER_REPAIRS; s.allowTerminalJunk = DEFAULT_ALLOW_TERMINAL_JUNK; s.checkAsciiStrings = DEFAULT_CHECK_ASCII_STRINGS; s.longStringsEnabled = DEFAULT_LONG_STRINGS_ENABLED; s.skipBlankAfterAssign = DEFAULT_SKIP_BLANK_AFTER_ASSIGN; s.useAsciiTables = DEFAULT_USE_ASCII_TABLES; s.useHierarch = DEFAULT_USE_HIERARCH; s.useUnicodeChars = DEFAULT_USE_UNICODE_CHARS; s.hierarchKeyFormatter = DEFAULT_HIERARCH_FORMATTER; s.hierarchKeyFormatter.setCaseSensitive(DEFAULT_CASE_SENSITIVE_HIERARCH); } /** * Sets whether 'D' may be used instead of 'E' to mark the exponent for a floating point value with precision beyond * that of a 32-bit float. * * @param allowExponentD if true D will be used instead of E to indicate the exponent of a decimal with * more precision than a 32-bit float. * * @since 1.16 * * @see #isUseExponentD() */ public static void setUseExponentD(boolean allowExponentD) { current().useExponentD = allowExponentD; } /** * Do we allow junk after a valid FITS file? * * @param allowTerminalJunk value to set * * @see #getAllowTerminalJunk() */ public static void setAllowTerminalJunk(boolean allowTerminalJunk) { current().allowTerminalJunk = allowTerminalJunk; } /** * Do we allow automatic header repairs, like missing end quotes? * * @param allowHeaderRepairs value to set * * @see #isAllowHeaderRepairs() */ public static void setAllowHeaderRepairs(boolean allowHeaderRepairs) { current().allowHeaderRepairs = allowHeaderRepairs; } /** * Enable/Disable checking of strings values used in tables to ensure that they are within the range specified by * the FITS standard. The standard only allows the values 0x20 - 0x7E with null bytes allowed in one limited * context. Disabled by default. * * @param checkAsciiStrings value to set * * @see #getCheckAsciiStrings() */ public static void setCheckAsciiStrings(boolean checkAsciiStrings) { current().checkAsciiStrings = checkAsciiStrings; } /** * There is not a real standard how to write hierarch keys, default we use the one where every key is separated by a * blank. If you want or need another format assing the formater here. * * @param formatter the hierarch key formatter. */ public static void setHierarchFormater(IHierarchKeyFormatter formatter) { current().hierarchKeyFormatter = formatter; } /** * Enable/Disable longstring support. * * @param longStringsEnabled value to set * * @see #isLongStringsEnabled() */ public static void setLongStringsEnabled(boolean longStringsEnabled) { current().longStringsEnabled = longStringsEnabled; } /** * If set to true the blank after the assign in the header cards in not written. The blank is stronly recommendet * but in some cases it is important that it can be ommitted. * * @param skipBlankAfterAssign value to set * * @deprecated The FITS standard is very explicit that assignment must be "= " (equals followed * by a blank space). It is also very specific that string values must have * their opening quote in byte 11 (counted from 1). If we allow skipping the * space, we will violate both standards in a way that is likely to break * compatibility with other tools. * * @see #isSkipBlankAfterAssign() */ @Deprecated public static void setSkipBlankAfterAssign(boolean skipBlankAfterAssign) { current().skipBlankAfterAssign = skipBlankAfterAssign; } /** * Indicate whether ASCII tables should be used where feasible. * * @param useAsciiTables value to set */ public static void setUseAsciiTables(boolean useAsciiTables) { current().useAsciiTables = useAsciiTables; } /** * Enable/Disable hierarchical keyword processing. * * @param useHierarch value to set */ public static void setUseHierarch(boolean useHierarch) { current().useHierarch = useHierarch; } /** *

* Enable/Disable writing char[] arrays as short[] in FITS binary tables (with column type * 'I'), instead of as standard FITS 1-byte ASCII characters (with column type 'A'). The old default of this library * has been to use unicode, and that behavior remains the default — the same as setting the argument to * true. On the flipside, setting it to false provides more convergence between the * handling of char[] columns and the nearly identical String columns, which have already * been restricted to ASCII before. *

* * @param value true to write char[] arrays as if short[] with column type * 'I' to binary tables (old behaviour, and hence default), or else false to write * them as byte[] with column type 'A', the same as for String (preferred * behaviour) * * @since 1.16 * * @see #isUseUnicodeChars() */ public static void setUseUnicodeChars(boolean value) { current().useUnicodeChars = value; } /** * Returns the common thread pool that we use for processing FITS files. * * @return the thread pool for processing FITS files. */ public static ExecutorService threadPool() { if (threadPool == null) { initializeThreadPool(); } return threadPool; } /** * Use thread local settings for the current thread instead of the global ones if the parameter is set to true, else * use the shared global settings. * * @param useThreadSettings true if the thread should not share the global settings. */ public static void useThreadLocalSettings(boolean useThreadSettings) { if (useThreadSettings) { LOCAL_SETTINGS.set(GLOBAL_SETTINGS.copy()); } else { LOCAL_SETTINGS.remove(); } } private static void initializeThreadPool() { synchronized (GLOBAL_SETTINGS) { if (threadPool == null) { threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2, // new ThreadFactory() { private int counter = 1; @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "nom-tam-fits worker " + counter++); thread.setDaemon(true); return thread; } }); } } } /** * Returns the current settings that guide how we read or produce FITS files. * * @return the current active settings for generating or interpreting FITS files. */ protected static FitsSettings current() { FitsSettings settings = LOCAL_SETTINGS.get(); if (settings == null) { return GLOBAL_SETTINGS; } return settings; } private FitsFactory() { } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/FitsHeap.java000066400000000000000000000204601476377620500234520ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.ByteArrayIO; import nom.tam.util.FitsDecoder; import nom.tam.util.FitsEncoder; /** * Heap for storing variable-length entries in binary tables. FITS binary tables store variable length arrays on a heap, * following the regular array data. The newer implementation of the heap now provides proper random access to the byte * buffer as of version 1.16. */ public class FitsHeap implements FitsElement { /** The minimum stoprage size to allocate for the heap, from which it can grow as necessary */ private static final int MIN_HEAP_CAPACITY = 16384; // TODO // AK: In principle we could use ReadWriteAccess interface as the storage, which can be either an in-memory // array or a buffered file region. The latter could support heaps over 2G, and could reduce memory overhead // for heap access in some future release... /** The underlying storage space of the heap */ private ByteArrayIO store; /** conversion from Java arrays to FITS binary representation */ private FitsEncoder encoder; /** conversion from FITS binary representation to Java arrays */ private FitsDecoder decoder; /** * Construct a new uninitialized FITS heap object. */ private FitsHeap() { } /** * Creates a heap of a given initial size. The new heap is initialized with 0's, and up to the specified number of * bytes are immediately available for reading (as zeroes). The heap can grow as needed if more data is written into * it. * * @throws IllegalArgumentException if the size argument is negative. */ FitsHeap(int size) throws IllegalArgumentException { if (size < 0) { throw new IllegalArgumentException("Illegal size for FITS heap: " + size); } ByteArrayIO data = new ByteArrayIO(Math.max(size, MIN_HEAP_CAPACITY)); data.setLength(Math.max(0, size)); setData(data); encoder = new FitsEncoder(store); decoder = new FitsDecoder(store); } /** * Sets the underlying data storage for this heap instance. Constructors should call this. * * @param data the new underlying storage object for this heap instance. */ protected synchronized void setData(ByteArrayIO data) { store = data; } /** * Add a copy constructor to allow us to duplicate a heap. This would be necessary if we wanted to copy an HDU that * included variable length columns. */ synchronized FitsHeap copy() { FitsHeap copy = new FitsHeap(); synchronized (copy) { copy.setData(store.copy()); copy.encoder = new FitsEncoder(copy.store); copy.decoder = new FitsDecoder(copy.store); } return copy; } /** * Gets data for a Java array from the heap. The array may be a multi-dimensional array of arrays. * * @param offset the heap byte offset at which the data begins. * @param array The array of primitives to be extracted. * * @throws FitsException if the operation failed */ public synchronized void getData(int offset, Object array) throws FitsException { try { store.position(offset); decoder.readArrayFully(array); } catch (Exception e) { throw new FitsException("Error decoding heap area at offset=" + offset + ", size=" + FitsEncoder.computeSize(array) + " (heap size " + size() + "): " + e.getMessage(), e); } } @Override public long getFileOffset() { throw new IllegalStateException("FitsHeap should only be reset from inside its parent, never alone"); } @Override public synchronized long getSize() { return size(); } /** * Puts data to the end of the heap. * * @param data a primitive array object, which may be multidimensional * * @return the number of bytes used by the data. * * @see #putData(Object, long) * @see #getData(int, Object) */ synchronized long putData(Object data) throws FitsException { return putData(data, store.length()); } /** * Puts data onto the heap at a specific heap position. * * @param data a primitive array object, which may be multidimensional * @param pos the byte offset at which the data should begin. * * @return the number of bytes used by the data. * * @see #putData(Object, long) * @see #getData(int, Object) */ synchronized long putData(Object data, long pos) throws FitsException { long lsize = pos + FitsEncoder.computeSize(data); if (lsize > Integer.MAX_VALUE) { throw new FitsException("FITS Heap > 2 G"); } try { store.position(pos); encoder.writeArray(data); } catch (Exception e) { throw new FitsException("Unable to write variable column length data: " + e.getMessage(), e); } return store.position() - pos; } /** * Copies a segment of data from another heap to the end of this heap * * @param src the heap to source data from * @param offset the byte offset of the data in the source heap * @param len the number of bytes to copy * * @return the position of the copied data in this heap. */ synchronized int copyFrom(FitsHeap src, int offset, int len) { int pos = (int) store.length(); store.setLength(pos + len); synchronized (src) { System.arraycopy(src.store.getBuffer(), offset, store.getBuffer(), pos, len); } return pos; } @Override public synchronized void read(ArrayDataInput str) throws FitsException { if (store.length() == 0) { return; } try { str.readFully(store.getBuffer(), 0, (int) store.length()); } catch (IOException e) { throw new FitsException("Error reading heap " + e.getMessage(), e); } } @Override public boolean reset() { throw new IllegalStateException("FitsHeap should only be reset from inside its parent, never alone"); } @Override public void rewrite() throws IOException, FitsException { throw new FitsException("FitsHeap should only be rewritten from inside its parent, never alone"); } @Override public boolean rewriteable() { return false; } /** * Returns the current heap size. * * @return the size of the heap in bytes */ public synchronized int size() { return (int) store.length(); } @Override public synchronized void write(ArrayDataOutput str) throws FitsException { try { str.write(store.getBuffer(), 0, (int) store.length()); } catch (IOException e) { throw new FitsException("Error writing heap:" + e.getMessage(), e); } } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/FitsIntegrityException.java000066400000000000000000000046211476377620500264330ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam.fits * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * Failed checksum verification. * * @author Attila Kovacs * @since 1.18.1 */ public class FitsIntegrityException extends FitsException { private static final long serialVersionUID = -221485588322280968L; /** * Creates a new checksum verification failure * * @param name * the type of checksum test that failed * @param got * the checksum value that was found * @param expected * the checksum value expected */ FitsIntegrityException(String name, long got, long expected) { super("Failed " + name + ": got " + got + ", expected " + expected); } /** * Creates a new checksum verification failure for a verification failure on * a specific HDU * * @param hduIndex * the zero-based index of the HDU within the FITS * @param cause * the undrlying checksum verification failure */ FitsIntegrityException(int hduIndex, FitsIntegrityException cause) { super("Corrupted HDU[" + hduIndex + "]: " + cause.getMessage(), cause); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/FitsUtil.java000066400000000000000000000732471476377620500235250ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.net.HttpURLConnection; import java.net.ProtocolException; import java.net.URL; import java.net.URLConnection; import java.text.ParsePosition; import java.util.ArrayList; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.util.ArrayDataOutput; import nom.tam.util.AsciiFuncs; import nom.tam.util.FitsDecoder; import nom.tam.util.FitsEncoder; import nom.tam.util.FitsIO; import nom.tam.util.RandomAccess; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Static utility functions used throughout the FITS classes. */ public final class FitsUtil { /** Lowest ASCII value that can be in FITS strings */ static final byte BLANK_SPACE = 0x20; /** Highest ASCII value that can be in FITS strings */ static final byte MIN_ASCII_VALUE = 0x20; /** Highest ASCII value that can be in FITS strings */ static final byte MAX_ASCII_VALUE = 0x7e; /** Highest ASCII value that can be in FITS strings */ static final byte ASCII_NULL = 0x00; /** * the logger to log to. */ private static final Logger LOG = Logger.getLogger(FitsUtil.class.getName()); private static boolean wroteCheckingError = false; /** * Utility class, do not instantiate it. */ private FitsUtil() { } /** * @deprecated use {@link #addPadding(long)} instead. Calculates the amount of padding needed to complete the * last FITS block at the specified current size. * * @return Total size of blocked FITS element, using e.v. padding to fits block size. * * @param size the current size. */ public static int addPadding(int size) { return size + padding(size); } /** * Calculates the amount of padding needed to complete the last FITS block at the specified current size. * * @return Total size of blocked FITS element, using e.v. padding to fits block size. * * @param size the current size. * * @see #padding(long) * @see #pad(ArrayDataOutput, long) */ public static long addPadding(long size) { return size + padding(size); } /** * Converts an array of boolean or {@link Boolean} values to FITS logicals (bytes containint 'T', 'F' * or '\0'). The shapes and size of the resulting array matches that of the input. Values of '\0' are converted to * null values. * * @param o a new array of Boolean * * @return an array of FITS logical values with the same size and shape as the input * * @see #bytesToBooleanObjects(Object) * @see #byteToBoolean(byte[]) * * @since 1.18 */ static Object booleansToBytes(Object o) { if (o == null) { return new byte[] {FitsEncoder.byteForBoolean(null)}; } if (o instanceof Boolean) { return new byte[] {FitsEncoder.byteForBoolean((Boolean) o)}; } if (o instanceof boolean[]) { boolean[] bool = (boolean[]) o; byte[] b = new byte[bool.length]; for (int i = 0; i < bool.length; i++) { b[i] = FitsEncoder.byteForBoolean(bool[i]); } return b; } if (o instanceof Boolean[]) { Boolean[] bool = (Boolean[]) o; byte[] b = new byte[bool.length]; for (int i = 0; i < bool.length; i++) { b[i] = FitsEncoder.byteForBoolean(bool[i]); } return b; } if (o instanceof Object[]) { Object[] array = (Object[]) o; Object[] b = null; for (int i = 0; i < array.length; i++) { Object e = booleansToBytes(array[i]); if (b == null) { b = (Object[]) Array.newInstance(e.getClass(), array.length); } b[i] = e; } return b; } throw new IllegalArgumentException("Not boolean values: " + o.getClass().getName()); } /** * Converts an array of FITS logicals (bytes containint 'T', 'F' or '\0') to an array of {@link Boolean}. The shapes * and size of the resulting array matches that of the input. Values of '\0' are converted to null * values. * * @param bytes and array of FITS logical values * * @return a new array of Boolean with the same size and shape as the input * * @see #booleansToBytes(Object) * * @since 1.18 */ static Object bytesToBooleanObjects(Object bytes) { if (bytes instanceof Byte) { return FitsDecoder.booleanObjectFor(((Number) bytes).intValue()); } if (bytes instanceof byte[]) { byte[] b = (byte[]) bytes; Boolean[] bool = new Boolean[b.length]; for (int i = 0; i < b.length; i++) { bool[i] = FitsDecoder.booleanObjectFor(b[i]); } return bool; } if (bytes instanceof Object[]) { Object[] array = (Object[]) bytes; Object[] bool = null; for (int i = 0; i < array.length; i++) { Object e = bytesToBooleanObjects(array[i]); if (bool == null) { bool = (Object[]) Array.newInstance(e.getClass(), array.length); } bool[i] = e; } return bool; } throw new IllegalArgumentException("Cannot convert to boolean values: " + bytes.getClass().getName()); } /** * Converts an array of booleans into the bits packed into a block of bytes. * * @param bits an array of bits * * @return a new byte array containing the packed bits (in big-endian order) * * @see #bytesToBits(Object) * * @since 1.18 */ static byte[] bitsToBytes(boolean[] bits) throws IllegalArgumentException { byte[] bytes = new byte[(bits.length + Byte.SIZE - 1) / Byte.SIZE]; for (int i = 0; i < bits.length; i++) { if (bits[i]) { int pos = Byte.SIZE - 1 - i % Byte.SIZE; bytes[i / Byte.SIZE] |= 1 << pos; } } return bytes; } /** * Converts an array of bit segments into the bits packed into a blocks of bytes. * * @param bits an array of bits * @param l the number of bits in a segment that are to be kept together * * @return a new byte array containing the packed bits (in big-endian order) * * @see #bytesToBits(Object) * * @since 1.18 */ static byte[] bitsToBytes(boolean[] bits, int l) throws IllegalArgumentException { int n = bits.length / l; // Number of bit segments int bl = (l + Byte.SIZE - 1) / Byte.SIZE; // Number of bytes per segment byte[] bytes = new byte[n * bl]; // The converted byte array for (int i = 0; i < n; i++) { int off = i * l; // bit offset int boff = i * bl; // byte offset for (int j = 0; j < l; j++) { if (bits[off + j]) { int pos = Byte.SIZE - 1 - j % Byte.SIZE; bytes[boff + (j / Byte.SIZE)] |= 1 << pos; } } } return bytes; } /** * Converts the bits packed into a block of bytes into a boolean array. * * @param bytes the byte array containing the packed bits (in big-endian order) * @param count the number of bits to extract * * @return an array of boolean with the separated bit values. * * @see #bitsToBytes(Object) * * @since 1.18 */ static boolean[] bytesToBits(byte[] bytes, int count) { boolean[] bits = new boolean[count]; for (int i = 0; i < bits.length; i++) { int pos = Byte.SIZE - 1 - i % Byte.SIZE; bits[i] = ((bytes[i / Byte.SIZE] >>> pos) & 1) == 1; } return bits; } /** * Extracts a string from a byte array at the specified offset, maximal length and termination byte. This method * trims trailing spaces but not leading ones. * * @param bytes an array of ASCII bytes * @param offset the array index at which the string begins * @param maxLen the maximum number of bytes to extract from the position * @param terminator the byte value that terminates the string, such as 0x00. * * @return a new String with the relevant bytes, with length not exceeding the specified limit. * * @since 1.18 */ static String extractString(byte[] bytes, ParsePosition pos, int maxLen, byte terminator) { int offset = pos.getIndex(); if (offset >= bytes.length) { return ""; } if (offset + maxLen > bytes.length) { maxLen = bytes.length - offset; } int end = -1; // Check up to the specified length or termination for (int i = 0; i < maxLen; i++) { byte b = bytes[offset + i]; pos.setIndex(offset + i); if (b == terminator || b == 0) { break; } if (b != BLANK_SPACE) { end = i; } } pos.setIndex(pos.getIndex() + 1); byte[] sanitized = new byte[end + 1]; boolean checking = FitsFactory.getCheckAsciiStrings(); // Up to the specified length or terminator for (int i = 0; i <= end; i++) { byte b = bytes[offset + i]; if (checking && (b < BLANK_SPACE || b > MAX_ASCII_VALUE)) { if (!wroteCheckingError) { LOG.warning("WARNING! Converting invalid table string character[s] to spaces."); wroteCheckingError = true; } b = BLANK_SPACE; } sanitized[i] = b; } return AsciiFuncs.asciiString(sanitized); } /** * Converts a FITS byte sequence to a Java string array, triming spaces at the heads and tails of each element. * While FITS typically considers leading spaces significant, this library has been removing them from regularly * shaped string arrays for a very long time, apparently based on request by users then... Even though it seems like * a bad choice, since users could always call {@link String#trim()} if they needed to, we cannot recover the * leading spaces once the string was trimmed. At this point we have no real choice but to continue the tradition, * lest we want to break exising applications, which may rely on this behavior. * * @return Convert bytes to Strings, removing leading and trailing spaces from each entry. * * @param bytes byte array to convert * @param maxLen the max string length * * @deprecated (for internal use) No longer used internally, will be removed in the future. */ public static String[] byteArrayToStrings(byte[] bytes, int maxLen) { // Note that if a String in a binary table contains an internal 0, // the FITS standard says that it is to be considered as terminating // the string at that point, so that software reading the // data back may not include subsequent characters. // No warning of this truncation is given. String[] res = new String[bytes.length / maxLen]; for (int i = 0; i < res.length; i++) { res[i] = extractString(bytes, new ParsePosition(i * maxLen), maxLen, (byte) '\0').trim(); } return res; } /** * Converts a FITS representation of boolean values as bytes to a java boolean array. This implementation does not * handle FITS null values. * * @return Convert an array of bytes to booleans. * * @param bytes the array of bytes to get the booleans from. * * @see FitsDecoder#booleanFor(int) */ static boolean[] byteToBoolean(byte[] bytes) { boolean[] bool = new boolean[bytes.length]; for (int i = 0; i < bytes.length; i++) { bool[i] = FitsDecoder.booleanFor(bytes[i]); } return bool; } /** * Parses a logical value from a string, using loose conversion. The string may contain either 'true'/'false' or * 'T'/'F' (case insensitive), or else a zero (false) or non-zero number value (true). All * other strings will return null corresponding to an undefined logical value. * * @param s A string * * @return true, false, or null (if undefined). */ @SuppressFBWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "null has specific meaning here") static Boolean parseLogical(String s) { if (s == null) { return null; } s = s.trim(); if (s.isEmpty()) { return null; } if (s.equalsIgnoreCase("true") || s.equalsIgnoreCase("t")) { return true; } if (s.equalsIgnoreCase("false") || s.equalsIgnoreCase("f")) { return false; } try { long l = Long.parseLong(s); return l != 0; } catch (NumberFormatException e) { // Nothing to do.... } try { double d = Double.parseDouble(s); if (!Double.isNaN(d)) { return d != 0; } } catch (NumberFormatException e) { // Nothing to do.... } return null; } /** * Gets the file offset for the given IO resource. * * @return The offset from the beginning of file (if random accessible), or -1 otherwise. * * @param o the stream to get the position * * @deprecated (for internal use) Visibility may be reduced to the package level in the future. */ public static long findOffset(Closeable o) { if (o instanceof RandomAccess) { return ((RandomAccess) o).getFilePointer(); } return -1; } /** * Gets and input stream for a given URL resource. * * @return Get a stream to a URL accommodating possible redirections. Note that if a redirection request * points to a different protocol than the original request, then the redirection is not * handled automatically. * * @param url the url to get the stream from * @param level max levels of redirection * * @throws IOException if the operation failed */ public static InputStream getURLStream(URL url, int level) throws IOException { URLConnection conn = null; int code = -1; try { conn = url.openConnection(); if (conn instanceof HttpURLConnection) { code = ((HttpURLConnection) conn).getResponseCode(); } return conn.getInputStream(); } catch (ProtocolException e) { LOG.log(Level.WARNING, "could not connect to " + url + (code >= 0 ? " got responce-code" + code : ""), e); throw e; } } /** * Returns the maximum string length in an array. * * @return the maximum length of string in an array. * * @param strings array of strings to check * * @deprecated (for internal use) No longer used internally, may be removed in the future. */ public static int maxLength(String[] strings) { int max = 0; for (String element : strings) { if (element != null && element.length() > max) { max = element.length(); } } return max; } /** * Returns the maximum string length in an array of strings. Non-string elements nd null values are ignored. * * @return the maximum length of strings in an array. * * @param o array of strings to check */ static int maxStringLength(Object o) { if (o instanceof String) { return ((String) o).length(); } int max = 0; if (o instanceof Object[]) { for (Object e : (Object[]) o) { if (e == null) { continue; } int l = maxStringLength(e); if (l > max) { max = l; } } } return max; } /** * Returns the minimum string length in an array of strings. Non-string elements nd null values are ignored. * * @return the minimum length of strings in an array. * * @param o strings array of strings to check */ static int minStringLength(Object o) { if (o instanceof String) { return ((String) o).length(); } int min = -1; if (o instanceof Object[]) { for (Object e : (Object[]) o) { if (e == null) { return 0; } int l = minStringLength(e); if (l == 0) { return 0; } if (min < 0 || l < min) { min = l; } } } return min < 0 ? 0 : min; } /** * Adds the necessary amount of padding needed to complete the last FITS block. * * @param stream stream to pad * @param size the current size of the stream (total number of bytes written to it since the beginning * of the FITS). * * @throws FitsException if the operation failed * * @see #pad(ArrayDataOutput, long, byte) * * @deprecated (for internal use) Visibility may be reduced to package level in the future */ public static void pad(ArrayDataOutput stream, long size) throws FitsException { pad(stream, size, (byte) 0); } /** * Adds the necessary amount of padding needed to complete the last FITS block., usign the designated padding byte * value. * * @param stream stream to pad * @param size the current size of the stream (total number of bytes written to it since the beginning * of the FITS). * @param fill the byte value to use for the padding * * @throws FitsException if the operation failed * * @see #pad(ArrayDataOutput, long) * * @deprecated (for internal use) Visibility may be reduced to private in the future */ public static void pad(ArrayDataOutput stream, long size, byte fill) throws FitsException { int len = padding(size); if (len > 0) { byte[] buf = new byte[len]; Arrays.fill(buf, fill); try { stream.write(buf); stream.flush(); } catch (Exception e) { throw new FitsException("Unable to write padding", e); } } } /** * @deprecated see Use {@link #padding(long)} instead. * * @return How many bytes are needed to fill a 2880 block? * * @param size the size without padding */ public static int padding(int size) { return padding((long) size); } /** * Calculated the amount of padding we need to add given the current size of a FITS file (under construction) * * @param size the current size of our FITS file before the padding * * @return the number of bytes of padding we need to add at the end to complete the FITS block. * * @see #addPadding(long) * @see #pad(ArrayDataOutput, long) */ public static int padding(long size) { int mod = (int) (size % FitsFactory.FITS_BLOCK_SIZE); if (mod > 0) { return FitsFactory.FITS_BLOCK_SIZE - mod; } return 0; } /** * Attempts to reposition a FITS input ot output. The call will succeed only if the underlying input or output is * random accessible. Othewise, an exception will be thrown. * * @deprecated This method wraps an {@link IOException} into a {@link FitsException} for no good * reason really. A revision of the API could reduce the visibility of this method, * and/or procees the underlying exception instead. * * @param o the FITS input or output * @param offset the offset to position it to. * * @throws FitsException if the underlying input/output is not random accessible or if the requested position is * invalid. */ @Deprecated public static void reposition(FitsIO o, long offset) throws FitsException { // TODO AK: argument should be RandomAccess instead of Closeable, since // that's the only type we actually handle... if (o == null) { throw new FitsException("Attempt to reposition null stream"); } if (!(o instanceof RandomAccess) || offset < 0) { throw new FitsException( "Invalid attempt to reposition stream " + o + " of type " + o.getClass().getName() + " to " + offset); } try { ((RandomAccess) o).seek(offset); } catch (IOException e) { throw new FitsException("Unable to repostion stream " + o + " of type " + o.getClass().getName() + " to " + offset + ": " + e.getMessage(), e); } } /** * Converts a string to ASCII bytes in the specified array, padding (with 0x00) or truncating as necessary to * provide the expected length at the specified arrya offset. * * @param s a string * @param res the byte array into which to extract the ASCII bytes * @param offset array index in the byte array at which the extracted bytes should begin * @param len the maximum number of bytes to extract, truncating or padding (with 0x00) as needed. * @param pad the byte value to use to pad the remainder of the string */ private static void stringToBytes(String s, byte[] res, int offset, int len, byte pad) { int l = 0; if (s != null) { byte[] b = AsciiFuncs.getBytes(s); l = Math.min(b.length, len); if (l > 0) { System.arraycopy(b, 0, res, offset, l); } } // Terminate and pad as necessary if (l < len) { Arrays.fill(res, offset + l, offset + len, pad); } } /** * Converts a string to an array of ASCII bytes, padding (with 0x00) or truncating as necessary to provide the * expected length. * * @param s a string * @param len the number of bytes for the return value, also the maximum number of bytes that are extracted from * the string * * @return a byte array of the specified length containing the truncated or padded string value. * * @see #stringsToByteArray(String[], int, byte) q * * @since 1.18 */ static byte[] stringToByteArray(String s, int len) { byte[] res = new byte[len]; stringToBytes(s, res, 0, len, BLANK_SPACE); return res; } /** * Convert an array of Strings to bytes. padding (with 0x00) or truncating as necessary to provide the expected * length. * * @return the resulting bytes * * @param stringArray the array with Strings * @param len the number of bytes used for each string element. The string will be truncated ot padded * as necessary to fit into that size. * * @deprecated (for internal use) Visibility may be reduced to package level in the future. */ public static byte[] stringsToByteArray(String[] stringArray, int len) { return stringsToByteArray(stringArray, len, BLANK_SPACE); } /** * Convert an array of Strings to bytes. padding (with 0x00) or truncating as necessary to provide the expected * length. * * @return the resulting bytes * * @param stringArray the array with Strings * @param len the number of bytes used for each string element. The string will be truncated ot padded as * necessary to fit into that size. * @param pad the byte value to use for padding strings as necessary to the requisite length. */ static byte[] stringsToByteArray(String[] stringArray, int len, byte pad) { byte[] res = new byte[stringArray.length * len]; for (int i = 0; i < stringArray.length; i++) { stringToBytes(stringArray[i], res, i * len, len, pad); } return res; } /** * Convert an array of strings to a delimited sequence of bytes, e.g. for sequentialized variable-sized storage of * multiple string elements. The last string component is terminated by an ASCII NUL (0x00). * * @return the resulting bytes * * @param array the array with Strings * @param maxlen the maximum string length or -1 of unlimited. * @param delim the byte value that delimits string components * * @see #stringToByteArray(String, int) * * @since 1.18 */ static byte[] stringsToDelimitedBytes(String[] array, int maxlen, byte delim) { int l = array.length - 1; for (String s : array) { l += (s == null) ? 1 : Math.max(s.length() + 1, maxlen); } byte[] b = new byte[l]; l = 0; for (String s : array) { if (s != null) { stringToBytes(s, b, l, s.length(), BLANK_SPACE); l += s.length(); } b[l++] = delim; } b[l - 1] = (byte) 0; return b; } /** * Extracts strings from a packed delimited byte sequence. Strings start either immediately after the prior string * reached its maximum length, or else immediately after the specified delimiter byte value. * * @param bytes bytes containing the packed strings * @param maxlen the maximum length of individual string components * @param delim the byte value that delimits strings shorter than the maximum length * * @return An array of the extracted strings * * @see #stringsToDelimitedBytes(String[], int, byte) * * @since 1.18 */ private static String[] delimitedBytesToStrings(byte[] bytes, byte delim) { ArrayList s = new ArrayList<>(); ParsePosition pos = new ParsePosition(0); while (pos.getIndex() < bytes.length) { s.add(extractString(bytes, pos, bytes.length, delim)); } String[] a = new String[s.size()]; s.toArray(a); return a; } /** * Extracts strings from a packed delimited byte sequence. Strings start either immediately after the prior string * reached its maximum length, or else immediately after the specified delimiter byte value. * * @param bytes bytes containing the packed strings * @param maxlen the maximum length of individual string components * @param delim the byte value that delimits strings shorter than the maximum length * * @return An array of the extracted strings * * @see #stringsToDelimitedBytes(String[], int, byte) * * @since 1.18 */ static String[] delimitedBytesToStrings(byte[] bytes, int maxlen, byte delim) { if (maxlen <= 0) { return delimitedBytesToStrings(bytes, delim); } String[] res = new String[(bytes.length + maxlen - 1) / maxlen]; ParsePosition pos = new ParsePosition(0); for (int i = 0; i < res.length; i++) { res[i] = extractString(bytes, pos, maxlen, delim); } return res; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/Header.java000066400000000000000000003365041476377620500231500ustar00rootroot00000000000000package nom.tam.fits; import java.io.EOFException; import java.io.IOException; import java.io.PrintStream; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.fits.FitsFactory.FitsSettings; import nom.tam.fits.header.Bitpix; import nom.tam.fits.header.Checksum; import nom.tam.fits.header.IFitsHeader; import nom.tam.fits.header.IFitsHeader.VALUE; import nom.tam.fits.header.Standard; import nom.tam.fits.utilities.FitsCheckSum; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.AsciiFuncs; import nom.tam.util.ComplexValue; import nom.tam.util.Cursor; import nom.tam.util.FitsIO; import nom.tam.util.FitsInputStream; import nom.tam.util.FitsOutput; import nom.tam.util.HashedList; import nom.tam.util.RandomAccess; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.fits.header.Standard.BITPIX; import static nom.tam.fits.header.Standard.BLANKS; import static nom.tam.fits.header.Standard.COMMENT; import static nom.tam.fits.header.Standard.END; import static nom.tam.fits.header.Standard.EXTEND; import static nom.tam.fits.header.Standard.GCOUNT; import static nom.tam.fits.header.Standard.GROUPS; import static nom.tam.fits.header.Standard.HISTORY; import static nom.tam.fits.header.Standard.NAXIS; import static nom.tam.fits.header.Standard.NAXISn; import static nom.tam.fits.header.Standard.PCOUNT; import static nom.tam.fits.header.Standard.SIMPLE; import static nom.tam.fits.header.Standard.TFIELDS; import static nom.tam.fits.header.Standard.XTENSION; import static nom.tam.fits.header.Standard.XTENSION_BINTABLE; import static nom.tam.fits.header.extra.CXCExt.LONGSTRN; /** *

* Access and manipulate the header of a HDU. FITS headers serve more than a single purpose: *

*
    *
  1. provide an essential description of the type, size, and layout of the HDUs data segment
  2. *
  3. describe the data as completely as possible via standardized (or conventional) keywords
  4. *
  5. provide storage for additional user-specific key / value pairs
  6. *
  7. allow for comments to aid human readability
  8. *
*

* First and foremost headers provide a description of the data object that follows the header in the HDU. Some of that * description is essential and critical to the integrity of the FITS file, such as the header keywords that describe * the type, size, and layout of the data segment. This library will automatically populate the header with appropriate * information using the mandatory keywords (such as SIMPLE or XTENSION, BITPIX, * NAXIS, NAXISn, PCOUNT, GCOUNT keywords, as well as * essential table column format descriptions). Users of the library should avoid overwriting these mandatory keywords * manually, since they may corrupt the FITS file, rendering it unreadable. *

*

* Beyond the keywords that describe the type, shape, and size of data, the library will not add further information to * the header. The users of the library are responsible to complete the header description as necessary. This includes * non-enssential data descriptions (such as EXTNAME, BUNIT, OBSERVER, or * optional table column descriptors TTYPEn, TUNITn, coordinate systems via the * appropriate WCS keywords, or checksums). Users of the library are responsible for completing the data description * using whatever standard or conventional keywords are available and appropriate. Please refer to the * FITS Standard documentation to see what typical * descriptions of data you might want to use. *

*

* Last but not least, the header is also a place where FITS creators can store (nearly) arbitrary key/value pairs. In * earlier versions of the FITS standard, header keywords were restricted to max. 8 upper case letters and numbers (plus * hyphen and underscore), and no more than 70 character value fields. However, as of FITS 4.0 (and even before as a * registered convention), string values of arbitrary length may be stored using the OGIP 1.0 long string convention, * while the ESO HIERARCH convention allows * keywords with more than 8 characters and hierarchical keywords. Support, conformance, and compliance to these * conventions can be toggled by static settings in {@link FitsFactory} to user preference. *

*

* As of version 1.16, we also support reserving space in headers for future additions using the * {@link #ensureCardSpace(int)} method, also part of the FITS 4.0 standard. It allows users to finish populating * headers after data that follows the header is already written -- a useful feature for recording data from * streaming sources. *

*/ @SuppressWarnings("deprecation") public class Header implements FitsElement { /** * The default character position to which comments should be aligned if possible (zero-based). The fITS standard * requires that 'fixed-format' values are right-justified to byte 30 (index 29 in Java), and recommends a space * after that before the comment. As such, comments should normally start at byte 30 (counted from 0). (We will add * a space at that position before the '/' indicating the comment start) */ public static final int DEFAULT_COMMENT_ALIGN = 30; /** * The earliest position (zero-based) at which a comment may start for a regular key/value entry. * * @deprecated We will disable changing alignment in the future because it may violate the standard for * 'fixed-format' header entries, and result in files that are unreadable by some other software. * This constant will be obsoleted and removed. */ public static final int MIN_COMMENT_ALIGN = 20; /** * The largest (zero-based) comment alignment allowed that can still contain some meaningful comment (word) * * @deprecated We will disable changing alignment in the future because it may violate the standard for * 'fixed-format' header entries, and result in files that are unreadable by some other software. * This constant will be obsoleted and removed. */ public static final int MAX_COMMENT_ALIGN = 70; /** * The alignment position of card comments for a more pleasing visual experience. Comments will be aligned to this * position, provided the lengths of all fields allow for it. */ private static int commentAlign = DEFAULT_COMMENT_ALIGN; private static final Logger LOG = Logger.getLogger(Header.class.getName()); private static final int MIN_NUMBER_OF_CARDS_FOR_VALID_HEADER = 4; /** * The actual header data stored as a HashedList of HeaderCard's. */ private final HashedList cards; /** Offset of this Header in the FITS file */ private long fileOffset; private List duplicates; private HashSet dupKeys; /** Input descriptor last time header was read */ private ArrayDataInput input; /** * The mimimum number of cards to write, including blank header space as described in the FITS 4.0 standard. */ private int minCards; /** * The number of bytes that this header occupied in file. (for re-writing). */ private long readSize; /** The checksum calculated from the input stream */ private long streamSum = -1L; /** * the sorter used to sort the header cards defore writing the header. */ private Comparator headerSorter; private BasicHDU owner; private boolean validating = false; /** * Keyword checking mode when adding standardized keywords via the {@link IFitsHeader} interface. * * @author Attila Kovacs * * @since 1.19 */ public enum KeywordCheck { /** No keyword checking will be performed. */ NONE, /** Check only that the keyword is appropriate for the type of data contained in the associated HDU */ DATA_TYPE, /** * Strict checking, will refuse to set mandatory FITS keywords -- which should normally be set by the library * alone. */ STRICT } /** * The keyword checking mode used by the library until the user changes it it. * * @since 1.19 */ public static final KeywordCheck DEFAULT_KEYWORD_CHECK_POLICY = KeywordCheck.DATA_TYPE; private static KeywordCheck defaultKeyCheck = DEFAULT_KEYWORD_CHECK_POLICY; private KeywordCheck keyCheck = defaultKeyCheck; /** * Create a header by reading the information from the input stream. * * @param dis The input stream to read the data from. * * @return null if there was a problem with the header; otherwise return the * header read from the input stream. * * @throws TruncatedFileException if the stream ended prematurely * @throws IOException if the header could not be read. */ public static Header readHeader(ArrayDataInput dis) throws TruncatedFileException, IOException { Header myHeader = new Header(); try { myHeader.read(dis); } catch (EOFException e) { // An EOF exception is thrown only if the EOF was detected // when reading the first card. In this case we want // to return a null. return null; } return myHeader; } /** * please use {@link FitsFactory#setLongStringsEnabled(boolean)} instead. * * @param flag the new value for long-string enabling. */ @Deprecated public static void setLongStringsEnabled(boolean flag) { FitsFactory.setLongStringsEnabled(flag); } /** Create a new header with the required default keywords for a standalone header. */ public Header() { cards = new HashedList<>(); headerSorter = new HeaderOrder(); duplicates = null; clear(); } /** * Create a header and populate it from the input stream * * @param is The input stream where header information is expected. * * @throws IOException if the header could not be read. * @throws TruncatedFileException if the stream ended prematurely */ public Header(ArrayDataInput is) throws TruncatedFileException, IOException { this(); read(is); } /** * Create a header which points to the given data object. * * @param o The data object to be described. * * @throws FitsException if the data was not valid for this header. */ public Header(Data o) throws FitsException { this(); o.fillHeader(this); } /** * Create a header and initialize it with a vector of strings. * * @param newCards Card images to be placed in the header. */ public Header(String[] newCards) { this(); for (String newCard : newCards) { cards.add(HeaderCard.create(newCard)); } } void assignTo(BasicHDU hdu) { // if (owner != null) { // throw new IllegalStateException("This header was already assigned to a HDU"); // } this.owner = hdu; } /** *

* Reserves header card space for populating at a later time. When written to a stream, the header will be large * enough to hold at least the specified number of cards. If the header has fewer physical cards then the remaining * space will be padded with blanks, leaving space for future additions, as specified by the FITS 4.0 standard for * preallocated header space. *

*

* This method is also called by {@link #read(ArrayDataInput)}, with the number of cards (including reserved blank * space) contained in the header input stream, in order to ensure that the header remains rewritable even if it is * shortened by the removal of cards (explicitly, or because they were duplicates). *

*

* A new setting always overrides prior ones. For example, calling this method with an argument that is %lt;=1 will * eliminate (reset) any prior preallocated header space. *

* * @param nCards the mimimum number of 80-character header records that is header must be able to support when * written to a stream, including preallocated blank header space. * * @since 1.16 * * @see #getMinimumSize() * @see #write(ArrayDataOutput) * @see #read(ArrayDataInput) * @see #resetOriginalSize() */ public void ensureCardSpace(int nCards) { if (nCards < 1) { nCards = 1; } minCards = nCards; } /** * Merges copies of all cards from another header, provided they are not readily present in this header. That is, it * merges only the non-conflicting or distinct header entries from the designated source (in contrast to * {@link #updateLines(Header)}). All comment cards are merged also (since these can always appear multiple times, * so they do not conflict). The merged entries are added at the end of the header, in the same order as they appear * in the source. The merged entries will be copies of the cards in the original, such that subsequent modifications * to the source will not affect this header or vice versa. * * @param source The header from which to inherit non-conflicting entries * * @since 1.19 * * @see #updateLines(Header) */ public void mergeDistinct(Header source) { seekTail(); Cursor c = source.iterator(); while (c.hasNext()) { HeaderCard card = c.next(); if (card.isCommentStyleCard() || !containsKey(card.getKey())) { if (card.getKey().equals(Standard.SIMPLE.key()) || card.getKey().equals(Standard.XTENSION.key())) { // Do not merge SIMPLE / XTENSION -- these are private matters... continue; } addLine(card.copy()); } } } /** * Insert a new header card at the current position, deleting any prior occurence of the same card while maintaining * the current position to point to after the newly inserted card. * * @param fcard The card to be inserted. * * @throws IllegalArgumentException if the current keyword checking mode does not allow the headercard with its * standard keyword in the header. * * @see #setKeywordChecking(KeywordCheck) */ public void addLine(HeaderCard fcard) throws IllegalArgumentException { if (fcard == null) { return; } if (fcard.getStandardKey() != null) { checkKeyword(fcard.getStandardKey()); } cursor().add(fcard); } /** *

* Sets the built-in standard keyword checking mode. When populating the header using {@link IFitsHeader} keywords * the library will check if the given keyword is appropriate for the type of HDU that the header represents, and * will throw an {@link IllegalArgumentException} if the specified keyword is not allowed for that type of HDU. *

*

* This method changes the keyword checking mode for this header instance only. If you want to change the mode for * all newly created headers globally, use {@link #setDefaultKeywordChecking(KeywordCheck)} instead. *

* * @param mode The keyword checking mode to use. * * @see #getKeywordChecking() * @see HeaderCard#setValueCheckingPolicy(nom.tam.fits.HeaderCard.ValueCheck) * * @since 1.19 */ public void setKeywordChecking(KeywordCheck mode) { keyCheck = mode; } /** * Sets the default mode of built-in standard keyword checking mode for new headers. When populating the header * using {@link IFitsHeader} keywords the library will check if the given keyword is appropriate for the type of HDU * that the header represents, and will throw an {@link IllegalArgumentException} if the specified keyword is not * allowed for that type of HDU. * * @param mode The keyword checking policy to use. * * @see #setKeywordChecking(KeywordCheck) * @see #getKeywordChecking() * @see HeaderCard#setValueCheckingPolicy(nom.tam.fits.HeaderCard.ValueCheck) * * @since 1.19 */ public static void setDefaultKeywordChecking(KeywordCheck mode) { defaultKeyCheck = mode; } /** * Returns the current keyword checking mode. * * @return the current keyword checking mode * * @see #setKeywordChecking(KeywordCheck) * * @since 1.19 */ public final KeywordCheck getKeywordChecking() { return keyCheck; } private void checkKeyword(IFitsHeader keyword) throws IllegalArgumentException { if (keyCheck == KeywordCheck.NONE || owner == null) { return; } if (keyCheck == KeywordCheck.STRICT && (keyword.status() == IFitsHeader.SOURCE.MANDATORY || keyword.status() == IFitsHeader.SOURCE.INTEGRAL)) { throw new IllegalArgumentException("Keyword " + keyword + " should be set by the library only"); } switch (keyword.hdu()) { case PRIMARY: if (!owner.canBePrimary()) { throw new IllegalArgumentException( "Keyword " + keyword + " is a primary keyword and may not be used in extensions"); } return; case EXTENSION: if (owner instanceof RandomGroupsHDU) { throw new IllegalArgumentException( "Keyword " + keyword + " is an extension keyword but random groups may only be primary"); } return; case IMAGE: if (owner instanceof ImageHDU || owner instanceof RandomGroupsHDU) { return; } break; case GROUPS: if (owner instanceof RandomGroupsHDU) { return; } break; case TABLE: if (owner instanceof TableHDU) { return; } break; case ASCII_TABLE: if (owner instanceof AsciiTableHDU) { return; } break; case BINTABLE: if (owner instanceof BinaryTableHDU) { return; } break; default: return; } throw new IllegalArgumentException( "Keyword " + keyword.key() + " is not appropriate for " + owner.getClass().getName()); } /** * Add or replace a key with the given boolean value and its standardized comment. If the value is not compatible * with the convention of the keyword, a warning message is logged but no exception is thrown (at this point). The * new card will be placed at the current mark position, as set e.g. by {@link #findCard(IFitsHeader)}. * * @param key The header key. * @param val The boolean value. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * @throws IllegalArgumentException If the keyword is invalid * * @see #addValue(String, Boolean, String) */ public HeaderCard addValue(IFitsHeader key, Boolean val) throws HeaderCardException, IllegalArgumentException { HeaderCard card = HeaderCard.create(key, val); addLine(card); return card; } /** * Add or replace a key with the given double value and its standardized comment. If the value is not compatible * with the convention of the keyword, a warning message is logged but no exception is thrown (at this point). The * new card will be placed at the current mark position, as set e.g. by {@link #findCard(IFitsHeader)}. * * @param key The header key. * @param val The double value. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * @throws IllegalArgumentException If the keyword is invalid * * @see #addValue(String, Number, String) */ public HeaderCard addValue(IFitsHeader key, Number val) throws HeaderCardException, IllegalArgumentException { HeaderCard card = HeaderCard.create(key, val); addLine(card); return card; } /** * Add or replace a key with the given string value and its standardized comment. If the value is not compatible * with the convention of the keyword, a warning message is logged but no exception is thrown (at this point). The * new card will be placed at the current mark position, as set e.g. by {@link #findCard(IFitsHeader)}. * * @param key The header key. * @param val The string value. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * @throws IllegalArgumentException If the keyword is invalid * * @see #addValue(String, String, String) */ public HeaderCard addValue(IFitsHeader key, String val) throws HeaderCardException, IllegalArgumentException { HeaderCard card = HeaderCard.create(key, val); addLine(card); return card; } /** * Add or replace a key with the given complex value and its standardized comment. If the value is not compatible * with the convention of the keyword, a warning message is logged but no exception is thrown (at this point). The * new card will be placed at the current mark position, as set e.g. by {@link #findCard(IFitsHeader)}. * * @param key The header key. * @param val The complex value. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * @throws IllegalArgumentException If the keyword is invalid * * @see #addValue(String, ComplexValue, String) * * @since 1.17 */ public HeaderCard addValue(IFitsHeader key, ComplexValue val) throws HeaderCardException, IllegalArgumentException { HeaderCard card = HeaderCard.create(key, val); addLine(card); return card; } /** * Add or replace a key with the given boolean value and comment. The new card will be placed at the current mark * position, as set e.g. by {@link #findCard(String)}. * * @param key The header key. * @param val The boolean value. * @param comment A comment to append to the card. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * * @see #addValue(IFitsHeader, Boolean) * @see HeaderCard#HeaderCard(String, Boolean, String) */ public HeaderCard addValue(String key, Boolean val, String comment) throws HeaderCardException { HeaderCard hc = new HeaderCard(key, val, comment); addLine(hc); return hc; } /** * Add or replace a key with the given number value and comment. The value will be represented in the header card * with use the native precision of the value or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS}, whichever * fits in the available card space. Trailing zeroes will be ommitted. The new card will be placed at the current * mark position, as set e.g. by {@link #findCard(String)}. * * @param key The header key. * @param val The number value. * @param comment A comment to append to the card. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * * @see #addValue(String, Number, int, String) * @see #addValue(IFitsHeader, Number) * @see HeaderCard#HeaderCard(String, Number, String) */ public HeaderCard addValue(String key, Number val, String comment) throws HeaderCardException { HeaderCard hc = new HeaderCard(key, val, comment); addLine(hc); return hc; } /** * Add or replace a key with the given number value and comment, using up to the specified decimal places after the * leading figure. Trailing zeroes will be ommitted. The new card will be placed at the current mark position, as * set e.g. by {@link #findCard(String)}. * * @param key The header key. * @param val The number value. * @param decimals The number of decimal places to show after the leading figure, or * {@link nom.tam.util.FlexFormat#AUTO_PRECISION} to use the native precision of the * value or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS}, whichever fits * in the available card space. * @param comment A comment to append to the card. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * * @see #addValue(String, Number, String) * @see HeaderCard#HeaderCard(String, Number, int, String) */ public HeaderCard addValue(String key, Number val, int decimals, String comment) throws HeaderCardException { HeaderCard hc = new HeaderCard(key, val, decimals, comment); addLine(hc); return hc; } /** * Add or replace a key with the given complex number value and comment. Trailing zeroes will be ommitted. The new * card will be placed at the current mark position, as set e.g. by {@link #findCard(String)}. * * @param key The header keyword. * @param val The complex number value. * @param comment A comment to append to the card. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * * @since 1.16 * * @see #addValue(String, ComplexValue, int, String) * @see HeaderCard#HeaderCard(String, ComplexValue, String) */ public HeaderCard addValue(String key, ComplexValue val, String comment) throws HeaderCardException { HeaderCard hc = new HeaderCard(key, val, comment); addLine(hc); return hc; } /** * Add or replace a key with the given complex number value and comment, using up to the specified decimal places * after the leading figure. Trailing zeroes will be ommitted. The new card will be placed at the current mark * position, as set e.g. by {@link #findCard(String)}. * * @param key The header keyword. * @param val The complex number value. * @param decimals The number of decimal places to show after the leading figure, or * {@link nom.tam.util.FlexFormat#AUTO_PRECISION} to use the native precision of the * value, or at least {@link nom.tam.util.FlexFormat#DOUBLE_DECIMALS}, whichever * fits in the available card space. * @param comment A comment to append to the card. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * * @since 1.16 * * @see #addValue(String, ComplexValue, String) * @see HeaderCard#HeaderCard(String, ComplexValue, int, String) */ public HeaderCard addValue(String key, ComplexValue val, int decimals, String comment) throws HeaderCardException { HeaderCard hc = new HeaderCard(key, val, decimals, comment); addLine(hc); return hc; } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. We will remove this method in the future. * * @param key The header key. * @param val The integer value. * @param comment A comment to append to the card. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * * @since 1.16 * * @see #addValue(String, Number, String) * @see HeaderCard#createHexValueCard(String, long) * @see #getHexValue(String) */ @Deprecated public HeaderCard addHexValue(String key, long val, String comment) throws HeaderCardException { HeaderCard hc = HeaderCard.createHexValueCard(key, val, comment); addLine(hc); return hc; } /** * Add or replace a key with the given string value and comment. The new card will be placed at the current mark * position, as set e.g. by {@link #findCard(String)}. * * @param key The header key. * @param val The string value. * @param comment A comment to append to the card. * * @return the new card that was added. * * @throws HeaderCardException If the parameters cannot build a valid FITS card. * * @see #addValue(IFitsHeader, String) * @see HeaderCard#HeaderCard(String, String, String) */ public HeaderCard addValue(String key, String val, String comment) throws HeaderCardException { HeaderCard hc = new HeaderCard(key, val, comment); addLine(hc); return hc; } /** * get a builder for filling the header cards using the builder pattern. * * @param key the key for the first card. * * @return the builder for header cards. */ public HeaderCardBuilder card(IFitsHeader key) { return new HeaderCardBuilder(this, key); } /** * Tests if the specified keyword is present in this table. * * @param key the keyword to be found. * * @return true if the specified keyword is present in this table; false otherwise. */ public final boolean containsKey(IFitsHeader key) { return cards.containsKey(key.key()); } /** * Tests if the specified keyword is present in this table. * * @param key the keyword to be found. * * @return true if the specified keyword is present in this table; false otherwise. */ public final boolean containsKey(String key) { return cards.containsKey(key); } /** * Delete the card associated with the given key. Nothing occurs if the key is not found. * * @param key The header key. */ public void deleteKey(IFitsHeader key) { deleteKey(key.key()); } /** * Delete the card associated with the given key. Nothing occurs if the key is not found. * * @param key The header key. */ public void deleteKey(String key) { // AK: This version will not move the current position to the deleted // key if (containsKey(key)) { cards.remove(cards.get(key)); } } /** * Print the header to a given stream. Note that this method does not show reserved card space before the END * keyword, and thus does not necessarily show the same layout as what would appear in a file. * * @param ps the stream to which the card images are dumped. * * @see #ensureCardSpace(int) */ public void dumpHeader(PrintStream ps) { Cursor iter = iterator(); while (iter.hasNext()) { ps.println(iter.next()); } } /** * Returns the card associated with a given key. Unlike {@link #findCard(IFitsHeader)}, it does not alter the mark * position at which new cards are added. * * @param key the header key. * * @return null if the keyword could not be found; return the HeaderCard object otherwise. * * @see #getCard(String) * @see #findCard(IFitsHeader) * * @since 1.18.1 */ public HeaderCard getCard(IFitsHeader key) { return this.getCard(key.key()); } /** * Find the card associated with a given key. If found this sets the mark (cursor) to the card, otherwise it unsets * the mark. The mark is where new cards will be added to the header by default. If you do not want to change the * mark position, use {@link #getCard(IFitsHeader)} instead. * * @param key The header key. * * @return null if the keyword could not be found; return the HeaderCard object otherwise. * * @see #getCard(IFitsHeader) * @see #findCard(String) */ public HeaderCard findCard(IFitsHeader key) { return this.findCard(key.key()); } /** * Returns the card associated with a given key. Unlike {@link #findCard(String)}, it does not alter the mark * position at which new cards are added. * * @param key the header key. * * @return null if the keyword could not be found; return the HeaderCard object otherwise. * * @see #getCard(IFitsHeader) * @see #findCard(String) * * @since 1.18.1 */ public HeaderCard getCard(String key) { return cards.get(key); } /** * Finds the card associated with a given key, and returns it. If found this sets the mark (cursor) to just before * the card, such that {@link #nextCard()} will return that very same card on the first subsequent call. If the * header contains no matching entry, the mark is reset to the tail of the header (the same as {@link #seekTail()}). * The mark determines where new cards will be added to the header by default. If you do not want to alter the mark * position, use {@link #getCard(String)} instead. * * @param key the header key. * * @return Returns the header entry for the given keyword, or null if the header has no such entry. * * @see #getCard(String) * @see #findCard(String) */ public HeaderCard findCard(String key) { HeaderCard card = cards.get(key); if (card != null) { cursor().setKey(key); } else { cursor().end(); } return card; } /************************************ * brief Collect the header cards that match a regular expression. This is useful if one needs to search for a * keyword that is buried under some HIERARCH string conventions of unspecified depth. So to search for some key * like "HIERARCH OBO SUBOBO MYOBO", which would appear with the key HIERARCH.OBO.SUBOBO.MYOBO in this FITS * implementation, one could search with regex="HIER.*MYOBO" and find it, supposed FitsFactory.setUseHierarch(true) * was called before creating the header. * * @param regex The generalized regular expression for the keyword search * * @return The list of header cards that match the regular expression. * * @author Richard J. Mathar * * @since 1.19.1 */ public HeaderCard[] findCards(final String regex) { /* * The collection of header cards that match. */ ArrayList crds = new ArrayList<>(); /* * position pointer to start of card stack and loop over all header cards */ nom.tam.util.Cursor iter = iterator(); while (iter.hasNext()) { final HeaderCard card = iter.next(); /* * compare with regular expression and add to output list if it does */ if (card.getKey().matches(regex)) { crds.add(card); } } HeaderCard[] tmp = new HeaderCard[crds.size()]; return crds.toArray(tmp); } /* findCards */ /** * @deprecated Use {@link #findCard(String)} or {@link #getCard(String)} instead. Find the card associated with * a given key. * * @param key The header key. * * @return null if the keyword could not be found; return the card image otherwise. */ @Deprecated public String findKey(String key) { HeaderCard card = findCard(key); if (card == null) { return null; } return card.toString(); } /** * Get the bid decimal value associated with the given key. * * @deprecated The FITS header does not support decimal types beyond those that can be represented by a 64-bit * IEEE double-precision floating point value. * * @param key The header key. * * @return The associated value or 0.0 if not found. */ public final BigDecimal getBigDecimalValue(IFitsHeader key) { return getBigDecimalValue(key.key()); } /** * Get the big decimal value associated with the given key. * * @deprecated The FITS header does not support decimal types beyond those that can be represented by a 64-bit * IEEE double-precision floating point value. * * @param key The header key. * @param dft The default value to return if the key cannot be found. * * @return the associated value. */ public final BigDecimal getBigDecimalValue(IFitsHeader key, BigDecimal dft) { return getBigDecimalValue(key.key(), dft); } /** * Get the big decimal value associated with the given key. * * @deprecated The FITS header does not support decimal types beyond those that can be represented by a 64-bit * IEEE double-precision floating point value. * * @param key The header key. * * @return The associated value or 0.0 if not found. */ public final BigDecimal getBigDecimalValue(String key) { return getBigDecimalValue(key, BigDecimal.ZERO); } /** * Get the big decimal value associated with the given key. * * @deprecated The FITS header does not support decimal types beyond those that can be represented by a 64-bit * IEEE double-precision floating point value. * * @param key The header key. * @param dft The default value to return if the key cannot be found. * * @return the associated value. */ public BigDecimal getBigDecimalValue(String key, BigDecimal dft) { HeaderCard fcard = getCard(key); if (fcard == null) { return dft; } return fcard.getValue(BigDecimal.class, dft); } /** * Get the big integer value associated with the given key. * * @deprecated The FITS header does not support integer types beyond those that can be represented by a 64-bit * integer. * * @param key The header key. * * @return the associated value or 0 if not found. */ public final BigInteger getBigIntegerValue(IFitsHeader key) { return getBigIntegerValue(key.key()); } /** * Get the big integer value associated with the given key, or return a default value. * * @deprecated The FITS header does not support integer types beyond those that can be represented by a 64-bit * integer. * * @param key The header key. * @param dft The default value to be returned if the key cannot be found. * * @return the associated value. */ public final BigInteger getBigIntegerValue(IFitsHeader key, BigInteger dft) { return getBigIntegerValue(key.key(), dft); } /** * Get the big integer value associated with the given key. * * @deprecated The FITS header does not support integer types beyond those that can be represented by a 64-bit * integer. * * @param key The header key. * * @return The associated value or 0 if not found. */ public final BigInteger getBigIntegerValue(String key) { return getBigIntegerValue(key, BigInteger.ZERO); } /** * Get the big integer value associated with the given key. * * @deprecated The FITS header does not support integer types beyond those that can be represented by a 64-bit * integer. * * @param key The header key. * @param dft The default value to be returned if the key cannot be found. * * @return the associated value. */ public BigInteger getBigIntegerValue(String key, BigInteger dft) { HeaderCard fcard = getCard(key); if (fcard == null) { return dft; } return fcard.getValue(BigInteger.class, dft); } /** * Get the complex number value associated with the given key. * * @param key The header key. * * @return The associated value or {@link ComplexValue#ZERO} if not found. * * @since 1.16 * * @see #getComplexValue(String, ComplexValue) * @see HeaderCard#getValue(Class, Object) * @see #addValue(String, ComplexValue, String) */ public final ComplexValue getComplexValue(String key) { return getComplexValue(key, ComplexValue.ZERO); } /** * Get the complex number value associated with the given key, or return a default value. * * @param key The header key. * @param dft The default value to return if the key cannot be found. * * @return the associated value. * * @since 1.16 * * @see #getComplexValue(String) * @see HeaderCard#getValue(Class, Object) * @see #addValue(String, ComplexValue, String) */ public ComplexValue getComplexValue(String key, ComplexValue dft) { HeaderCard fcard = getCard(key); if (fcard == null) { return dft; } return fcard.getValue(ComplexValue.class, dft); } /** * Get the boolean value associated with the given key. * * @param key The header key. * * @return The value found, or false if not found or if the keyword is not a logical keyword. */ public final boolean getBooleanValue(IFitsHeader key) { return getBooleanValue(key.key()); } /** * Get the boolean value associated with the given key. * * @param key The header key. * @param dft The value to be returned if the key cannot be found or if the parameter does not seem to be a * boolean. * * @return the associated value. */ public final boolean getBooleanValue(IFitsHeader key, boolean dft) { return getBooleanValue(key.key(), dft); } /** * Get the boolean value associated with the given key. * * @param key The header key. * * @return The value found, or false if not found or if the keyword is not a logical keyword. */ public final boolean getBooleanValue(String key) { return getBooleanValue(key, false); } /** * Get the boolean value associated with the given key. * * @param key The header key. * @param dft The value to be returned if the key cannot be found or if the parameter does not seem to be a * boolean. * * @return the associated value. */ public boolean getBooleanValue(String key, boolean dft) { HeaderCard fcard = getCard(key); if (fcard == null) { return dft; } return fcard.getValue(Boolean.class, dft).booleanValue(); } /** * Get the n'th card image in the header * * @param n the card index to get * * @return the card image; return null if the n'th card does not exist. * * @deprecated An iterator from {@link #iterator(int)} or {@link #iterator()} should be used for sequential access * to the header. */ @Deprecated public String getCard(int n) { if (n >= 0 && n < cards.size()) { return cards.get(n).toString(); } return null; } /** * Return the size of the data including any needed padding. * * @return the data segment size including any needed padding. */ public long getDataSize() { return FitsUtil.addPadding(trueDataSize()); } /** * Get the double value associated with the given key. * * @param key The header key. * * @return The associated value or 0.0 if not found. */ public final double getDoubleValue(IFitsHeader key) { return getDoubleValue(key.key()); } /** * Get the double value associated with the given key, or return a default value. * * @param key The header key. * @param dft The default value to return if the key cannot be found. * * @return the associated value. */ public final double getDoubleValue(IFitsHeader key, double dft) { return getDoubleValue(key.key(), dft); } /** * Get the double value associated with the given key. * * @param key The header key. * * @return The associated value or 0.0 if not found. */ public final double getDoubleValue(String key) { return getDoubleValue(key, 0.0); } /** * Get the double value associated with the given key, or return a default value. * * @param key The header key. * @param dft The default value to return if the key cannot be found. * * @return the associated value. */ public double getDoubleValue(String key, double dft) { HeaderCard fcard = getCard(key); if (fcard == null) { return dft; } return fcard.getValue(Double.class, dft).doubleValue(); } /** *

* Returns the list of duplicate cards in the order they appeared in the parsed header. You can access the first * occurence of each of every duplicated FITS keyword using the usual Header.getValue(), and find * further occurrences in the list returned here. *

*

* The FITS standared strongly discourages using the keywords multiple times with assigned values, and specifies * that the values of such keywords are undefined by definitions. Our library is thus far more tolerant than the * FITS standard, allowing you to access each and every value that was specified for the same keyword. *

*

* On the other hand FITS does not limit how many times you can add comment-style keywords to a header. If you must * used the same keyword multiple times in your header, you should consider using comment-style entries instead. *

* * @return the list of duplicate cards. Note that when the header is read in, only the last entry for a given * keyword is retained in the active header. This method returns earlier cards that have been discarded * in the order in which they were encountered in the header. It is possible for there to be many cards * with the same keyword in this list. * * @see #hadDuplicates() * @see #getDuplicateKeySet() */ public List getDuplicates() { return duplicates; } /** * Returns the set of keywords that had more than one value assignment in the parsed header. * * @return the set of header keywords that were assigned more than once in the same header, or null if * there were no duplicate assignments. * * @see #hadDuplicates() * @see #getDuplicates() * * @since 1.17 */ public Set getDuplicateKeySet() { return dupKeys; } @Override public long getFileOffset() { return fileOffset; } /** * Get the float value associated with the given key. * * @param key The header key. * * @return The associated value or 0.0 if not found. */ public final float getFloatValue(IFitsHeader key) { return getFloatValue(key.key()); } /** * Get the float value associated with the given key, or return a default value. * * @return the float value associated with the given key. * * @param key The header key. * @param dft The value to be returned if the key is not found. */ public final float getFloatValue(IFitsHeader key, float dft) { return getFloatValue(key.key(), dft); } /** * Get the float value associated with the given key. * * @param key The header key. * * @return The associated value or 0.0 if not found. */ public final float getFloatValue(String key) { return getFloatValue(key, 0.0F); } /** * Get the float value associated with the given key, or return a default value. * * @return the float value associated with the given key. * * @param key The header key. * @param dft The value to be returned if the key is not found. */ public float getFloatValue(String key, float dft) { HeaderCard fcard = getCard(key); if (fcard == null) { return dft; } return fcard.getValue(Float.class, dft).floatValue(); } /** * Get the int value associated with the given key. * * @param key The header key. * * @return The associated value or 0 if not found. */ public final int getIntValue(IFitsHeader key) { return (int) getLongValue(key); } /** * Get the int value associated with the given key, or return a default value. * * @return the value associated with the key as an int. * * @param key The header key. * @param dft The value to be returned if the key is not found. */ public final int getIntValue(IFitsHeader key, int dft) { return (int) getLongValue(key, dft); } /** * Get the int value associated with the given key. * * @param key The header key. * * @return The associated value or 0 if not found. */ public final int getIntValue(String key) { return (int) getLongValue(key); } /** * Get the int value associated with the given key, or return a default value. * * @return the value associated with the key as an int. * * @param key The header key. * @param dft The value to be returned if the key is not found. */ public int getIntValue(String key, int dft) { return (int) getLongValue(key, dft); } /** * Get the n'th key in the header. * * @param n the index of the key * * @return the card image; return null if the n'th key does not exist. * * @deprecated An iterator from {@link #iterator(int)} or {@link #iterator()} should be used for sequential access * to the header. */ @Deprecated public String getKey(int n) { if (n >= 0 && n < cards.size()) { return cards.get(n).getKey(); } return null; } /** * Get the long value associated with the given key. * * @param key The header key. * * @return The associated value or 0 if not found. */ public final long getLongValue(IFitsHeader key) { return getLongValue(key.key()); } /** * Get the long value associated with the given key, or return a default value. * * @param key The header key. * @param dft The default value to be returned if the key cannot be found. * * @return the associated value. */ public final long getLongValue(IFitsHeader key, long dft) { return getLongValue(key.key(), dft); } /** * Get the long value associated with the given key. * * @param key The header key. * * @return The associated value or 0 if not found. */ public final long getLongValue(String key) { return getLongValue(key, 0L); } /** * Get the long value associated with the given key, or return a default value. * * @param key The header key. * @param dft The default value to be returned if the key cannot be found. * * @return the associated value. */ public long getLongValue(String key, long dft) { HeaderCard fcard = getCard(key); if (fcard == null) { return dft; } return fcard.getValue(Long.class, dft).longValue(); } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a misreading of the * standard itself. * * @param key The header key. * * @return The associated value or 0 if not found. * * @since 1.16 * * @see #getHexValue(String, long) * @see HeaderCard#getHexValue() * @see #addHexValue(String, long, String) */ @Deprecated public final long getHexValue(String key) { return getHexValue(key, 0L); } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a misreading of the * standard itself. * * @param key The header key. * @param dft The default value to be returned if the key cannot be found. * * @return the associated value. * * @since 1.16 * * @see #getHexValue(String) * @see HeaderCard#getHexValue() * @see #addHexValue(String, long, String) */ public long getHexValue(String key, long dft) { HeaderCard fcard = getCard(key); if (fcard == null) { return dft; } try { return fcard.getHexValue(); } catch (NumberFormatException e) { return dft; } } /** * Returns the nominal number of currently defined cards in this header. Each card can consist of one or more * 80-character wide header records. * * @return the number of nominal cards in the header * * @see #getNumberOfPhysicalCards() */ public int getNumberOfCards() { return cards.size(); } /** * Returns the number of 80-character header records in this header, including an END marker (whether or not it is * currently contained). * * @return the number of physical cards in the header, including the END marker. * * @see #getNumberOfCards() * @see #getSize() */ public int getNumberOfPhysicalCards() { int count = 0; for (HeaderCard card : cards) { count += card.cardSize(); } // AK: Count the END card, which may not have been added yet... if (!containsKey(END)) { count++; } return count; } /** * Returns the minimum number of bytes that will be written by this header, either as the original byte size of a * header that was read, or else the minimum preallocated capacity after setting {@link #ensureCardSpace(int)}. * * @return the minimum byte size for this header. The actual header may take up more space than that (but never * less!), depending on the number of cards contained. * * @since 1.16 * * @see #ensureCardSpace(int) * @see #read(ArrayDataInput) */ public long getMinimumSize() { return FitsUtil.addPadding((long) minCards * HeaderCard.FITS_HEADER_CARD_SIZE); } /** * @deprecated for internal use) It should be a private method in the future. Returns the original size of * the header in the stream from which it was read. * * @return the size of the original header in bytes, or 0 if the header was not read from a stream. * * @see #read(ArrayDataInput) * @see #getMinimumSize() */ @Deprecated public final long getOriginalSize() { return readSize; } @Override public final long getSize() { if (!isValidHeader()) { return 0; } return FitsUtil .addPadding((long) Math.max(minCards, getNumberOfPhysicalCards()) * HeaderCard.FITS_HEADER_CARD_SIZE); } /** * Get the String value associated with the given standard key. * * @param key The standard header key. * * @return The associated value or null if not found or if the value is not a string. * * @see #getStringValue(String) * @see #getStringValue(IFitsHeader, String) */ public final String getStringValue(IFitsHeader key) { return getStringValue(key.key()); } /** * Get the String value associated with the given standard key, or return a default value. * * @param key The standard header key. * @param dft The default value. * * @return The associated value or the default value if not found or if the value is not a string. * * @see #getStringValue(String, String) * @see #getStringValue(IFitsHeader) */ public final String getStringValue(IFitsHeader key, String dft) { return getStringValue(key.key(), dft); } /** * Get the String value associated with the given key. * * @param key The header key. * * @return The associated value or null if not found or if the value is not a string. * * @see #getStringValue(IFitsHeader) * @see #getStringValue(String, String) */ public final String getStringValue(String key) { return getStringValue(key, null); } /** * Get the String value associated with the given key, or return a default value. * * @param key The header key. * @param dft The default value. * * @return The associated value or the default value if not found or if the value is not a string. * * @see #getStringValue(IFitsHeader, String) * @see #getStringValue(String) */ public String getStringValue(String key, String dft) { HeaderCard fcard = getCard(key); if (fcard == null || !fcard.isStringValue()) { return dft; } return fcard.getValue(); } /** * Checks if the header had duplicate assignments in the FITS. * * @return Were duplicate header keys found when this record was read in? */ public boolean hadDuplicates() { return duplicates != null; } /** * Adds a line to the header using the COMMENT style, i.e., no '=' in column 9. The comment text may be truncated to * fit into a single record, which is returned. Alternatively, you can split longer comments among multiple * consecutive cards of the same type by {@link #insertCommentStyleMultiline(String, String)}. * * @param key The comment style header keyword, or null for an empty comment line. * @param comment A string comment to follow. Illegal characters will be replaced by '?' and the comment may be * truncated to fit into the card-space (71 characters). * * @return The new card that was inserted, or null if the keyword itself was invalid or the * comment was null. * * @see #insertCommentStyleMultiline(String, String) * @see HeaderCard#createCommentStyleCard(String, String) */ public HeaderCard insertCommentStyle(String key, String comment) { if (comment == null) { comment = ""; } else if (comment.length() > HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH) { comment = comment.substring(0, HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH); LOG.warning("Truncated comment to fit card: [" + comment + "]"); } try { HeaderCard hc = HeaderCard.createCommentStyleCard(key, HeaderCard.sanitize(comment)); cursor().add(hc); return hc; } catch (HeaderCardException e) { LOG.log(Level.WARNING, "Ignoring comment card with invalid key [" + HeaderCard.sanitize(key) + "]", e); return null; } } /** * Adds a line to the header using the COMMENT style, i.e., no '=' in column 9. If the comment does not fit in a * single record, then it will be split (wrapped) among multiple consecutive records with the same keyword. Wrapped * lines will end with '&' (not itself a standard) to indicate comment cards that might belong together. * * @param key The comment style header keyword, or null for an empty comment line. * @param comment A string comment to follow. Illegal characters will be replaced by '?' and the comment may be * split among multiple records as necessary to be fully preserved. * * @return The number of cards inserted. * * @since 1.16 * * @see #insertCommentStyle(String, String) * @see #insertComment(String) * @see #insertUnkeyedComment(String) * @see #insertHistory(String) */ public int insertCommentStyleMultiline(String key, String comment) { // Empty comments must have at least one space char to write at least one // comment card... if ((comment == null) || comment.isEmpty()) { comment = " "; } int n = 0; for (int from = 0; from < comment.length();) { int to = from + HeaderCard.MAX_COMMENT_CARD_COMMENT_LENGTH; String part = null; if (to < comment.length()) { part = comment.substring(from, --to) + "&"; } else { part = comment.substring(from); } if (insertCommentStyle(key, part) == null) { return n; } from = to; n++; } return n; } /** * Adds one or more consecutive COMMENT records, wrapping the comment text as necessary. * * @param value The comment. * * @return The number of consecutive COMMENT cards that were inserted * * @see #insertCommentStyleMultiline(String, String) * @see #insertUnkeyedComment(String) * @see #insertHistory(String) * @see HeaderCard#createCommentCard(String) */ public int insertComment(String value) { return insertCommentStyleMultiline(COMMENT.key(), value); } /** * Adds one or more consecutive comment records with no keyword (bytes 1-9 left blank), wrapping the comment text as * necessary. * * @param value The comment. * * @return The number of consecutive comment-style cards with no keyword (blank keyword) that were inserted. * * @since 1.16 * * @see #insertCommentStyleMultiline(String, String) * @see #insertComment(String) * @see #insertHistory(String) * @see HeaderCard#createUnkeyedCommentCard(String) * @see #insertBlankCard() */ public int insertUnkeyedComment(String value) { return insertCommentStyleMultiline(BLANKS.key(), value); } /** * Adds a blank card into the header. * * @since 1.16 * * @see #insertUnkeyedComment(String) */ public void insertBlankCard() { insertCommentStyle(null, null); } /** * Adds one or more consecutive a HISTORY records, wrapping the comment text as necessary. * * @param value The history record. * * @return The number of consecutive HISTORY cards that were inserted * * @see #insertCommentStyleMultiline(String, String) * @see #insertComment(String) * @see #insertUnkeyedComment(String) * @see HeaderCard#createHistoryCard(String) */ public int insertHistory(String value) { return insertCommentStyleMultiline(HISTORY.key(), value); } /** * Returns a cursor-based iterator for this header's entries starting at the first entry. * * @return an iterator over the header cards */ public Cursor iterator() { return cards.iterator(0); } /** * Returns a cursor-based iterator for this header's entries. * * @deprecated We should never use indexed access to the header. This function will be removed in 2.0. * * @return an iterator over the header cards starting at an index * * @param index the card index to start the iterator */ @Deprecated public Cursor iterator(int index) { return cards.iterator(index); } /** * Return the iterator that represents the current position in the header. This provides a connection between * editing headers through Header add/append/update methods, and via Cursors, which can be used side-by-side while * maintaining desired card ordering. For the reverse direction ( translating iterator position to current position * in the header), we can just use findCard(). * * @return the iterator representing the current position in the header. * * @see #iterator() */ private Cursor cursor() { return cards.cursor(); } /** * Move the cursor to the end of the header. Subsequently, all addValue() calls will add new cards to * the end of the header. * * @return the cursor after it has been repositioned to the end * * @since 1.18.1 * * @see #seekTail() * @see #findCard(String) * @see #nextCard() */ public Cursor seekHead() { Cursor c = cursor(); while (c.hasPrev()) { c.prev(); } return c; } /** * Move the cursor to the end of the header. Subsequently, all addValue() calls will add new cards to * the end of the header. * * @return the cursor after it has been repositioned to the end * * @since 1.18.1 * * @see #seekHead() * @see #findCard(String) */ public Cursor seekTail() { cursor().end(); return cursor(); } /** * @deprecated (for internal use) Normally we either want to write a Java object to FITS (in * which case we have the dataand want to make a header for it), or we read some data * from a FITS input. In either case, there is no benefit of exposing such a function * as this to the user. * * @return Create the data element corresponding to the current header * * @throws FitsException if the header did not contain enough information to detect the type of the data */ @Deprecated public Data makeData() throws FitsException { return FitsFactory.dataFactory(this); } /** * Returns the header card at the currently set mark position and increments the mark position by one. The mark * position determines the location at which new entries are added to the header. The mark is set either to just * prior a particular card (e.g. via {@link #findCard(IFitsHeader)}. * * @return the next card in the Header using the built-in iterator * * @see #prevCard() * @see #findCard(IFitsHeader) * @see #findCard(String) * @see #seekHead() */ public HeaderCard nextCard() { if (cursor().hasNext()) { return cursor().next(); } return null; } /** * Returns the header card prior to the currently set mark position and decrements the mark position by one. The * mark position determines the location at which new entries are added to the header. The mark is set either to * just prior a particular card (e.g. via {@link #findCard(IFitsHeader)}. * * @return the next card in the Header using the built-in iterator * * @see #nextCard() * @see #findCard(IFitsHeader) * @see #findCard(String) * @see #seekHead() * * @since 1.18.1 */ public HeaderCard prevCard() { if (cursor().hasPrev()) { return cursor().prev(); } return null; } /** * Create a header which points to the given data object. * * @param o The data object to be described. * * @throws FitsException if the data was not valid for this header. * * @deprecated Use the appropriate Header constructor instead. Will remove in a future * releae. */ @Deprecated public void pointToData(Data o) throws FitsException { o.fillHeader(this); } /** * Remove all cards and reset the header to its default status. */ private void clear() { cards.clear(); duplicates = null; dupKeys = null; readSize = 0; fileOffset = -1; minCards = 0; } /** * Checks if the header is empty, that is if it contains no cards at all. * * @return true if the header contains no cards, otherwise false. * * @since 1.16 */ public boolean isEmpty() { return cards.isEmpty(); } /** *

* Reads new header data from an input, discarding any prior content. *

*

* As of 1.16, the header is ensured to (re)write at least the same number of cards as before, padding with blanks * as necessary, unless the user resets the preallocated card space with a call to {@link #ensureCardSpace(int)}. *

* * @param dis The input stream to read the data from. * * @throws TruncatedFileException the the stream ended prematurely * @throws IOException if the operation failed * * @see #ensureCardSpace(int) */ @Override public void read(ArrayDataInput dis) throws TruncatedFileException, IOException { // AK: Start afresh, in case the header had prior contents from before. clear(); if (dis instanceof RandomAccess) { fileOffset = FitsUtil.findOffset(dis); } else { fileOffset = -1; } if (dis instanceof FitsInputStream) { ((FitsInputStream) dis).nextChecksum(); } streamSum = -1L; int trailingBlanks = 0; minCards = 0; HeaderCardCountingArrayDataInput cardCountingArray = new HeaderCardCountingArrayDataInput(dis); try { for (;;) { HeaderCard fcard = new HeaderCard(cardCountingArray); minCards += fcard.cardSize(); // AK: Note, 'key' can never be null, as per contract of getKey(). So no need to check... String key = fcard.getKey(); if (isEmpty()) { checkFirstCard(key); } else if (fcard.isBlank()) { // AK: We don't add the trailing blank cards, but keep count of them. // (esp. in case the aren't trailing...) trailingBlanks++; continue; } else if (END.key().equals(key)) { addLine(fcard); break; // Out of reading the header. } else if (LONGSTRN.key().equals(key)) { // We don't check the value here. If the user // wants to be sure that long strings are disabled, // they can call setLongStringsEnabled(false) after // reading the header. FitsFactory.setLongStringsEnabled(true); } // AK: The preceding blank spaces were internal, not trailing // so add them back in now... for (int i = 0; i < trailingBlanks; i++) { insertBlankCard(); } trailingBlanks = 0; if (cards.containsKey(key)) { addDuplicate(cards.get(key)); } addLine(fcard); } } catch (EOFException e) { // Normal end-of-file before END key... throw e; } catch (Exception e) { if (isEmpty() && FitsFactory.getAllowTerminalJunk()) { // If this happened where we expect a new header to start, then // treat is as if end-of-file if terminal junk is allowed forceEOF( "Junk detected where header was expected to start" + ((fileOffset > 0) ? ": at " + fileOffset : ""), e); } if (e instanceof TruncatedFileException) { throw (TruncatedFileException) e; } throw new IOException("Invalid FITS Header" + (isEmpty() ? e : ":\n\n --> Try FitsFactory.setAllowTerminalJunk(true) prior to reading to work around.\n"), e); } if (fileOffset >= 0) { input = dis; } ensureCardSpace(cardCountingArray.getPhysicalCardsRead()); readSize = FitsUtil.addPadding((long) minCards * HeaderCard.FITS_HEADER_CARD_SIZE); // Read to the end of the current FITS block. // try { dis.skipAllBytes(FitsUtil.padding(minCards * HeaderCard.FITS_HEADER_CARD_SIZE)); } catch (EOFException e) { // No biggy. We got a complete header just fine, it's only that there was no // padding before EOF. We'll just log that, but otherwise keep going. LOG.log(Level.WARNING, "Premature end-of-file: no padding after header.", e); } if (dis instanceof FitsInputStream) { streamSum = ((FitsInputStream) dis).nextChecksum(); } // AK: Log if the file ends before the expected end-of-header position. if (Fits.checkTruncated(dis)) { // No biggy. We got a complete header just fine, it's only that there was no // padding before EOF. We'll just log that, but otherwise keep going. LOG.warning("Premature end-of-file: no padding after header."); } // Move the cursor to after the last card -- this is where new cards will be added. seekTail(); } /** * Returns the random-accessible input from which this header was read, or null if the header is not * associated with an input, or the input is not random accessible. * * @return the random-accessible input associated with this header or null * * @see #read(ArrayDataInput) * * @since 1.18.1 */ RandomAccess getRandomAccessInput() { return (input instanceof RandomAccess) ? (RandomAccess) input : null; } /** * Returns the checksum value calculated duting reading from a stream. It is only populated when reading from * {@link FitsInputStream} imputs, and never from other types of inputs. Valid values are greater or equal to zero. * Thus, the return value will be -1L to indicate an invalid (unpopulated) checksum. * * @return the non-negative checksum calculated for the data read from a stream, or else -1L if the * data was not read from the stream. * * @see FitsInputStream * @see Data#getStreamChecksum() * * @since 1.18.1 */ final long getStreamChecksum() { return streamSum; } /** * Forces an EOFException to be thrown when some other exception happened, essentially treating the exception to * force a normal end the reading of the header. * * @param message the message to log. * @param cause the exception encountered while reading the header * * @throws EOFException the EOFException we'll throw instead. */ private void forceEOF(String message, Exception cause) throws EOFException { LOG.log(Level.WARNING, message, cause); throw new EOFException("Forced EOF at " + fileOffset + " due to: " + message); } /** * Delete a key. * * @param key The header key. * * @throws HeaderCardException if the operation failed * * @deprecated (duplicate method) Use {@link #deleteKey(String)} instead. */ @Deprecated public void removeCard(String key) throws HeaderCardException { deleteKey(key); } @Override public boolean reset() { try { FitsUtil.reposition(input, fileOffset); return true; } catch (Exception e) { LOG.log(Level.WARNING, "Exception while repositioning " + input, e); return false; } } /** * @deprecated Use {@link #ensureCardSpace(int)} with 1 as the argument instead. *

* Resets any prior preallocated header space, such as was explicitly set by * {@link #ensureCardSpace(int)}, or when the header was read from a stream to ensure it remains * rewritable, if possible. *

*

* For headers read from a stream, this will affect {@link #rewriteable()}, so users should not call * this method unless they do not intend to {@link #rewrite()} this header into the original FITS. *

* * @see #ensureCardSpace(int) * @see #read(ArrayDataInput) * @see #getMinimumSize() * @see #rewriteable() * @see #rewrite() */ @Deprecated public final void resetOriginalSize() { ensureCardSpace(1); } @Override public void rewrite() throws FitsException, IOException { ArrayDataOutput dos = (ArrayDataOutput) input; if (!rewriteable()) { throw new FitsException("Invalid attempt to rewrite Header."); } FitsUtil.reposition(dos, fileOffset); write(dos); dos.flush(); } @Override public boolean rewriteable() { long writeSize = FitsUtil .addPadding((long) Math.max(minCards, getNumberOfPhysicalCards()) * HeaderCard.FITS_HEADER_CARD_SIZE); return fileOffset >= 0 && input instanceof ArrayDataOutput && writeSize == getOriginalSize(); } /** * Set the BITPIX value for the header. The following values are permitted by FITS conventions: *
    *
  • 8 -- signed byte data. Also used for tables.
  • *
  • 16 -- signed short data.
  • *
  • 32 -- signed int data.
  • *
  • 64 -- signed long data.
  • *
  • -32 -- IEEE 32 bit floating point numbers.
  • *
  • -64 -- IEEE 64 bit floating point numbers.
  • *
* * @deprecated Use the safer {@link #setBitpix(Bitpix)} instead. * * @param val The value set by the user. * * @throws IllegalArgumentException if the value is not a valid BITPIX value. * * @see #setBitpix(Bitpix) */ @Deprecated public void setBitpix(int val) throws IllegalArgumentException { try { setBitpix(Bitpix.forValue(val)); } catch (FitsException e) { throw new IllegalArgumentException("Invalid BITPIX value: " + val, e); } } /** * Sets a standard BITPIX value for the header. * * @deprecated (for internall use) Visibility will be reduced to the package level in the future. * * @param bitpix The predefined enum value, e.g. {@link Bitpix#INTEGER}. * * @since 1.16 * * @see #setBitpix(int) */ @Deprecated public void setBitpix(Bitpix bitpix) { Cursor iter = iterator(); iter.next(); iter.add(bitpix.getHeaderCard()); } /** * Overwite the default header card sorter. * * @param headerSorter the sorter tu use or null to disable sorting */ public void setHeaderSorter(Comparator headerSorter) { this.headerSorter = headerSorter; } /** * Set the value of the NAXIS keyword * * @deprecated (for internal use) Visibility will be reduced to the package level in the future. * * @param val The dimensionality of the data. */ @Deprecated public void setNaxes(int val) { Cursor iter = iterator(); iter.setKey(BITPIX.key()); if (iter.hasNext()) { iter.next(); } iter.add(HeaderCard.create(NAXIS, val)); } /** * Set the dimension for a given axis. * * @deprecated (for internal use) Visibility will be reduced to the package level in the future. * * @param axis The axis being set. * @param dim The dimension */ @Deprecated public void setNaxis(int axis, int dim) { Cursor iter = iterator(); if (axis <= 0) { LOG.warning("setNaxis ignored because axis less than 0"); return; } if (axis == 1) { iter.setKey(NAXIS.key()); } else if (axis > 1) { iter.setKey(NAXISn.n(axis - 1).key()); } if (iter.hasNext()) { iter.next(); } iter.add(HeaderCard.create(NAXISn.n(axis), dim)); } /** * Set the SIMPLE keyword to the given value. * * @deprecated (for internall use) Visibility will be reduced to the package level in the future. * * @param val true for the primary header, otherwise false */ @Deprecated public void setSimple(boolean val) { deleteKey(SIMPLE); deleteKey(XTENSION); deleteKey(EXTEND); Cursor iter = iterator(); iter.add(HeaderCard.create(SIMPLE, val)); // If we're flipping back to and from the primary header // we need to add in the EXTEND keyword whenever we become // a primary, because it's not permitted in the extensions // (at least not where it needs to be in the primary array). if (findCard(NAXIS) != null) { if (findCard(NAXISn.n(getIntValue(NAXIS))) != null) { iter.next(); } } iter.add(HeaderCard.create(EXTEND, true)); } /** * Set the XTENSION keyword to the given value. * * @deprecated (for internall use) Visibility will be reduced to the package level * in the future. * * @param val The name of the extension. * * @throws IllegalArgumentException if the string value contains characters that are not allowed in FITS * headers, that is characters outside of the 0x20 thru 0x7E range. */ @Deprecated public void setXtension(String val) throws IllegalArgumentException { deleteKey(SIMPLE); deleteKey(XTENSION); deleteKey(EXTEND); iterator().add(HeaderCard.create(XTENSION, val)); } /** * @return the number of cards in the header * * @deprecated use {@link #getNumberOfCards()}. The units of the size of the header may be unclear. */ @Deprecated public int size() { return cards.size(); } /** * Update a valued entry in the header, or adds a new header entry. If the header does not contain a prior entry for * the specific keyword, or if the keyword is a comment-style key, a new entry is added at the current editing * position. Otherwise, the matching existing entry is updated in situ. * * @param key The key of the card to be replaced (or added). * @param card A new card * * @throws HeaderCardException if the operation failed */ public void updateLine(IFitsHeader key, HeaderCard card) throws HeaderCardException { updateLine(key.key(), card); } private void updateValue(IFitsHeader key, Boolean value) { HeaderCard prior = cards.get(key.key()); if (prior != null) { prior.setValue(value); } else { addValue(key, value); } } private void updateValue(IFitsHeader key, Number value) { HeaderCard prior = cards.get(key.key()); if (prior != null) { prior.setValue(value); } else { addValue(key, value); } } private void updateValue(IFitsHeader key, String value) { HeaderCard prior = cards.get(key.key()); if (prior != null) { prior.setValue(value); } else { addValue(key, value); } } /** * Update an existing card in situ, without affecting the current position, or else add a new card at the current * position. * * @param key The key of the card to be replaced. * @param card A new card * * @throws HeaderCardException if the operation failed */ public final void updateLine(String key, HeaderCard card) throws HeaderCardException { // Remove an existing card with the matching 'key' (even if that key // isn't the same // as the key of the card argument!) cards.update(key, card); } /** * Overwrite the lines in the header. Add the new PHDU header to the current one. If keywords appear twice, the new * value and comment overwrite the current contents. By Richard J Mathar. * * @param newHdr the list of new header data lines to replace the current ones. * * @throws HeaderCardException if the operation failed * * @see #mergeDistinct(Header) */ public void updateLines(final Header newHdr) throws HeaderCardException { Cursor j = newHdr.iterator(); while (j.hasNext()) { HeaderCard card = j.next(); if (card.isCommentStyleCard()) { insertCommentStyle(card.getKey(), card.getComment()); } else { updateLine(card.getKey(), card); } } } /** * Writes a number of blank header records, for example to create preallocated blank header space as described by * the FITS 4.0 standard. * * @param dos the output stream to which the data is to be written. * @param n the number of blank records to add. * * @throws IOException if there was an error writing to the stream * * @since 1.16 * * @see #ensureCardSpace(int) */ private void writeBlankCards(ArrayDataOutput dos, int n) throws IOException { byte[] blank = new byte[HeaderCard.FITS_HEADER_CARD_SIZE]; Arrays.fill(blank, (byte) ' '); while (--n >= 0) { dos.write(blank); } } /** * Add required keywords, and removes conflicting ones depending on whether it is designated as a primary header or * not. * * @param xType The value for the XTENSION keyword, or null if primary HDU. * * @throws FitsException if there was an error trying to edit the header. * * @since 1.17 * * @see #validate(FitsOutput) */ void setRequiredKeys(String xType) throws FitsException { if (xType == null) { // Delete keys that cannot be in primary deleteKey(XTENSION); // Some FITS readers don't like the PCOUNT and GCOUNT keywords in the primary header if (!getBooleanValue(GROUPS, false)) { deleteKey(PCOUNT); deleteKey(GCOUNT); } // Make sure we have SIMPLE updateValue(SIMPLE, true); } else { // Delete keys that cannot be in extensions deleteKey(SIMPLE); // Some FITS readers don't like the EXTEND keyword in extensions. deleteKey(EXTEND); // Make sure we have XTENSION updateValue(XTENSION, xType); } // Make sure we have BITPIX updateValue(BITPIX, getIntValue(BITPIX, Bitpix.VALUE_FOR_INT)); int naxes = getIntValue(NAXIS, 0); updateValue(NAXIS, naxes); for (int i = 1; i <= naxes; i++) { IFitsHeader naxisi = NAXISn.n(i); updateValue(naxisi, getIntValue(naxisi, 1)); } if (xType == null) { updateValue(EXTEND, true); } else { updateValue(PCOUNT, getIntValue(PCOUNT, 0)); updateValue(GCOUNT, getIntValue(GCOUNT, 1)); } } /** * Validates this header by making it a proper primary or extension header. In both cases it means adding required * keywords if missing, and removing conflicting cards. Then ordering is checked and corrected as necessary and * ensures that the END card is at the tail. * * @param asPrimary true if this header is to be a primary FITS header * * @throws FitsException If there was an issue getting the header into proper form. * * @since 1.17 */ public void validate(boolean asPrimary) throws FitsException { setRequiredKeys(asPrimary ? null : getStringValue(XTENSION, "UNKNOWN")); validate(); } /** * Validates the header making sure it has the required keywords and that the essential keywords appeat in the in * the required order * * @throws FitsException If there was an issue getting the header into proper form. */ private void validate() throws FitsException { // Ensure that all cards are in the proper order. if (headerSorter != null) { cards.sort(headerSorter); } checkBeginning(); checkEnd(); updateChecksum(); } private void updateChecksum() throws FitsException { if (containsKey(Checksum.CHECKSUM)) { HeaderCard dsum = getCard(Checksum.DATASUM); if (dsum != null) { FitsCheckSum.setDatasum(this, dsum.getValue(Long.class, 0L)); } else { deleteKey(Checksum.CHECKSUM); } } } /** * (for internal use) Similar to {@link #write(ArrayDataOutput)}, but writes the header as is, without * ensuring that mandatory keys are present, and in the correct order, or that checksums are updated. * * @param out The output file or stream to which to write * * @throws FitsException if there was a violation of the FITS standard * @throws IOException if the output was not accessible * * @since 1.20.1 * * @see #write(ArrayDataOutput) * @see #validate(boolean) */ public void writeUnchecked(ArrayDataOutput out) throws FitsException, IOException { FitsSettings settings = FitsFactory.current(); fileOffset = FitsUtil.findOffset(out); Cursor writeIterator = cards.iterator(0); int size = 0; while (writeIterator.hasNext()) { HeaderCard card = writeIterator.next(); byte[] b = AsciiFuncs.getBytes(card.toString(settings)); size += b.length; if (END.key().equals(card.getKey()) && minCards * HeaderCard.FITS_HEADER_CARD_SIZE > size) { // AK: Add preallocated blank header space before the END key. writeBlankCards(out, minCards - size / HeaderCard.FITS_HEADER_CARD_SIZE); size = minCards * HeaderCard.FITS_HEADER_CARD_SIZE; } out.write(b); } FitsUtil.pad(out, size, (byte) ' '); out.flush(); } @Override public void write(ArrayDataOutput out) throws FitsException { validate(); try { writeUnchecked(out); } catch (IOException e) { throw new FitsException("IO Error writing header", e); } } private void addDuplicate(HeaderCard dup) { // AK: Don't worry about duplicates for comment-style cards in general. if (dup.isCommentStyleCard()) { return; } if (duplicates == null) { duplicates = new ArrayList<>(); dupKeys = new HashSet<>(); } if (!dupKeys.contains(dup.getKey())) { HeaderCardParser.getLogger().log(Level.WARNING, "Multiple occurrences of key:" + dup.getKey()); dupKeys.add(dup.getKey()); } duplicates.add(dup); } /** * Check if the given key is the next one available in the header. */ private void cardCheck(Cursor iter, IFitsHeader key) throws FitsException { cardCheck(iter, key.key()); } /** * Check if the given key is the next one available in the header. */ private void cardCheck(Cursor iter, String key) throws FitsException { if (!iter.hasNext()) { throw new FitsException("Header terminates before " + key); } HeaderCard card = iter.next(); if (!card.getKey().equals(key)) { throw new FitsException("Key " + key + " not found where expected." + "Found " + card.getKey()); } } private void checkFirstCard(String key) throws FitsException { // AK: key cannot be null by the caller already, so checking for it makes dead code. if (!SIMPLE.key().equals(key) && !XTENSION.key().equals(key)) { throw new FitsException("Not a proper FITS header: " + HeaderCard.sanitize(key) + " at " + fileOffset); } } private void doCardChecks(Cursor iter, boolean isTable, boolean isExtension) throws FitsException { cardCheck(iter, BITPIX); cardCheck(iter, NAXIS); int nax = getIntValue(NAXIS); for (int i = 1; i <= nax; i++) { cardCheck(iter, NAXISn.n(i)); } if (isExtension) { cardCheck(iter, PCOUNT); cardCheck(iter, GCOUNT); if (isTable) { cardCheck(iter, TFIELDS); } } // This does not check for the EXTEND keyword which // if present in the primary array must immediately follow // the NAXISn. } /** * Ensure that the header begins with a valid set of keywords. Note that we do not check the values of these * keywords. */ private void checkBeginning() throws FitsException { Cursor iter = iterator(); if (!iter.hasNext()) { throw new FitsException("Empty Header"); } HeaderCard card = iter.next(); String key = card.getKey(); if (!key.equals(SIMPLE.key()) && !key.equals(XTENSION.key())) { throw new FitsException("No SIMPLE or XTENSION at beginning of Header"); } boolean isTable = false; boolean isExtension = false; if (key.equals(XTENSION.key())) { String value = card.getValue(); if (value == null || value.isEmpty()) { throw new FitsException("Empty XTENSION keyword"); } isExtension = true; if (value.equals(XTENSION_BINTABLE) || value.equals("A3DTABLE") || value.equals("TABLE")) { isTable = true; } } doCardChecks(iter, isTable, isExtension); Bitpix.fromHeader(this, false); } /** * Ensure that the header has exactly one END keyword in the appropriate location. */ private void checkEnd() { // Ensure we have an END card only at the end of the // header. Cursor iter = iterator(); HeaderCard card; while (iter.hasNext()) { card = iter.next(); if (!card.isKeyValuePair() && card.getKey().equals(END.key())) { iter.remove(); } } // End cannot have a comment iter.add(HeaderCard.createCommentStyleCard(END.key(), null)); } /** * Is this a valid header. * * @return true for a valid header, false otherwise. */ // TODO retire? private boolean isValidHeader() { if (getNumberOfCards() < MIN_NUMBER_OF_CARDS_FOR_VALID_HEADER) { return false; } Cursor iter = iterator(); String key = iter.next().getKey(); if (!key.equals(SIMPLE.key()) && !key.equals(XTENSION.key())) { return false; } key = iter.next().getKey(); if (!key.equals(BITPIX.key())) { return false; } key = iter.next().getKey(); if (!key.equals(NAXIS.key())) { return false; } while (iter.hasNext()) { key = iter.next().getKey(); } return key.equals(END.key()); } /** * @deprecated Use {@link NullDataHDU} instead. Create a header for a null image. */ @Deprecated void nullImage() { Cursor iter = iterator(); iter.add(HeaderCard.create(SIMPLE, true)); iter.add(Bitpix.BYTE.getHeaderCard()); iter.add(HeaderCard.create(NAXIS, 0)); iter.add(HeaderCard.create(EXTEND, true)); } /** * Find the end of a set of keywords describing a column or axis (or anything else terminated by an index). This * routine leaves the header ready to add keywords after any existing keywords with the index specified. The user * should specify a prefix to a keyword that is guaranteed to be present. */ Cursor positionAfterIndex(IFitsHeader prefix, int col) { String colnum = String.valueOf(col); cursor().setKey(prefix.n(col).key()); if (cursor().hasNext()) { // Bug fix (references to forward) here by Laurent Borges boolean toFar = false; while (cursor().hasNext()) { String key = cursor().next().getKey().trim(); // AK: getKey() cannot return null so no need to check. if (key.length() <= colnum.length() || !key.substring(key.length() - colnum.length()).equals(colnum)) { toFar = true; break; } } if (toFar) { cursor().prev(); // Gone one too far, so skip back an element. } } return cursor(); } /** * Replace the key with a new key. Typically this is used when deleting or inserting columns. If the convention of * the new keyword is not compatible with the existing value a warning message is logged but no exception is thrown * (at this point). * * @param oldKey The old header keyword. * @param newKey the new header keyword. * * @return true if the card was replaced. * * @throws HeaderCardException If newKey is not a valid FITS keyword. */ boolean replaceKey(IFitsHeader oldKey, IFitsHeader newKey) throws HeaderCardException { if (oldKey.valueType() == VALUE.NONE) { throw new IllegalArgumentException("cannot replace comment-style " + oldKey.key()); } HeaderCard card = getCard(oldKey); VALUE newType = newKey.valueType(); if (card != null && oldKey.valueType() != newType && newType != VALUE.ANY) { Class type = card.valueType(); Exception e = null; // Check that the exisating cards value is compatible with the expected type of the new key. if (newType == VALUE.NONE) { e = new IllegalArgumentException( "comment-style " + newKey.key() + " cannot replace valued key " + oldKey.key()); } else if (Boolean.class.isAssignableFrom(type) && newType != VALUE.LOGICAL) { e = new IllegalArgumentException(newKey.key() + " cannot not support the existing boolean value."); } else if (String.class.isAssignableFrom(type) && newType != VALUE.STRING) { e = new IllegalArgumentException(newKey.key() + " cannot not support the existing string value."); } else if (ComplexValue.class.isAssignableFrom(type) && newType != VALUE.COMPLEX) { e = new IllegalArgumentException(newKey.key() + " cannot not support the existing complex value."); } else if (card.isDecimalType() && newType != VALUE.REAL && newType != VALUE.COMPLEX) { e = new IllegalArgumentException(newKey.key() + " cannot not support the existing decimal values."); } else if (Number.class.isAssignableFrom(type) && newType != VALUE.REAL && newType != VALUE.INTEGER && newType != VALUE.COMPLEX) { e = new IllegalArgumentException(newKey.key() + " cannot not support the existing numerical value."); } if (e != null) { LOG.log(Level.WARNING, e.getMessage(), e); } } return replaceKey(oldKey.key(), newKey.key()); } /** * Replace the key with a new key. Typically this is used when deleting or inserting columns so that TFORMx -> * TFORMx-1 * * @param oldKey The old header keyword. * @param newKey the new header keyword. * * @return true if the card was replaced. * * @exception HeaderCardException If newKey is not a valid FITS keyword. TODO should be private */ boolean replaceKey(String oldKey, String newKey) throws HeaderCardException { HeaderCard oldCard = getCard(oldKey); if (oldCard == null) { return false; } if (!cards.replaceKey(oldKey, newKey)) { throw new HeaderCardException("Duplicate key [" + newKey + "] in replace"); } try { oldCard.changeKey(newKey); } catch (IllegalArgumentException e) { throw new HeaderCardException("New key [" + newKey + "] is invalid or too long for existing value.", e); } return true; } /** * Calculate the unpadded size of the data segment from the header information. * * @return the unpadded data segment size. */ private long trueDataSize() { // AK: No need to be too strict here. We can get a data size even if the // header isn't 100% to spec, // as long as the necessary keys are present. So, just check for the // required keys, and no more... if (!containsKey(BITPIX.key()) || !containsKey(NAXIS.key())) { return 0L; } int naxis = getIntValue(NAXIS, 0); // If there are no axes then there is no data. if (naxis == 0) { return 0L; } int[] axes = new int[naxis]; for (int axis = 1; axis <= naxis; axis++) { axes[axis - 1] = getIntValue(NAXISn.n(axis), 0); } boolean isGroup = getBooleanValue(GROUPS, false); int pcount = getIntValue(PCOUNT, 0); int gcount = getIntValue(GCOUNT, 1); int startAxis = 0; if (isGroup && naxis > 1 && axes[0] == 0) { startAxis = 1; } long size = 1; for (int i = startAxis; i < naxis; i++) { size *= axes[i]; } size += pcount; size *= gcount; // Now multiply by the number of bits per pixel and // convert to bytes. size *= Math.abs(getIntValue(BITPIX, 0)) / FitsIO.BITS_OF_1_BYTE; return size; } /** *

* Sets whether warnings about FITS standard violations are logged when a header is being read (parsed). Enabling * this feature can help identifying various standard violations in existing FITS headers, which nevertheless do not * prevent the successful reading of the header by this library. *

*

* If {@link FitsFactory#setAllowHeaderRepairs(boolean)} is set false, this will affect only minor * violations (e.g. a misplaced '=', missing space after '=', non-standard characters in header etc.), which * nevertheless do not interfere with the unamiguous parsing of the header information. More severe standard * violations, where some guessing may be required about the intent of some malformed header record, will throw * appropriate exceptions. If, however, {@link FitsFactory#setAllowHeaderRepairs(boolean)} is set true, * the parsing will throw fewer exceptions, and the additional issues may get logged as additional warning instead. * * @param value true if parser warnings about FITS standard violations when reading in existing FITS * headers are to be logged, otherwise false * * @see #isParserWarningsEnabled() * @see FitsFactory#setAllowHeaderRepairs(boolean) * * @since 1.16 */ public static void setParserWarningsEnabled(boolean value) { Level level = value ? Level.WARNING : Level.SEVERE; HeaderCardParser.getLogger().setLevel(level); Logger.getLogger(ComplexValue.class.getName()).setLevel(level); } /** * Checks whether warnings about FITS standard violations are logged when a header is being read (parsed). * * @return true if parser warnings about FITS standard violations when reading in existing FITS headers * are enabled and logged, otherwise false * * @see #setParserWarningsEnabled(boolean) * * @since 1.16 */ public static boolean isParserWarningsEnabled() { return !HeaderCardParser.getLogger().getLevel().equals(Level.SEVERE); } /** * Returns the current preferred alignment character position of inline header comments. This is the position at * which the '/' is placed for the inline comment. #deprecated * * @return The current alignment position for inline comments. * * @see #setCommentAlignPosition(int) * * @since 1.17 */ public static int getCommentAlignPosition() { return commentAlign; } /** * Sets a new alignment position for inline header comments. * * @param pos [20:70] The character position to which inline comments should be aligned if * possible. * * @throws IllegalArgumentException if the position is outside of the allowed range. * * @see #getCommentAlignPosition() * * @deprecated Not recommended as it may violate the FITS standart for 'fixed-format' * header entries, and make our FITS files unreadable by software that * expects strict adherence to the standard. We will remove this feature in * the future. * * @since 1.17 */ public static void setCommentAlignPosition(int pos) throws IllegalArgumentException { if (pos < Header.MIN_COMMENT_ALIGN || pos > Header.MAX_COMMENT_ALIGN) { throw new IllegalArgumentException( "Comment alignment " + pos + " out of range (" + MIN_COMMENT_ALIGN + ":" + MAX_COMMENT_ALIGN + ")."); } commentAlign = pos; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HeaderCard.java000066400000000000000000002606331476377620500237410ustar00rootroot00000000000000/* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ package nom.tam.fits; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; import java.util.logging.Logger; import nom.tam.fits.FitsFactory.FitsSettings; import nom.tam.fits.header.IFitsHeader; import nom.tam.fits.header.NonStandard; import nom.tam.fits.header.hierarch.IHierarchKeyFormatter; import nom.tam.util.ArrayDataInput; import nom.tam.util.AsciiFuncs; import nom.tam.util.ComplexValue; import nom.tam.util.CursorValue; import nom.tam.util.FitsInputStream; import nom.tam.util.FlexFormat; import nom.tam.util.InputReader; import static nom.tam.fits.header.Standard.BLANKS; import static nom.tam.fits.header.Standard.COMMENT; import static nom.tam.fits.header.Standard.CONTINUE; import static nom.tam.fits.header.Standard.HISTORY; /** * An individual entry in the FITS header, such as a key/value pair with an optional comment field, or a comment-style * entry without a value field. */ public class HeaderCard implements CursorValue, Cloneable { private static final Logger LOG = Logger.getLogger(HeaderCard.class.getName()); /** The number of characters per header card (line). */ public static final int FITS_HEADER_CARD_SIZE = 80; /** Maximum length of a FITS keyword field */ public static final int MAX_KEYWORD_LENGTH = 8; /** The length of two single quotes that must surround string values. */ public static final int STRING_QUOTES_LENGTH = 2; /** Maximum length of a FITS value field. */ public static final int MAX_VALUE_LENGTH = 70; /** Maximum length of a comment-style card comment field. */ public static final int MAX_COMMENT_CARD_COMMENT_LENGTH = MAX_VALUE_LENGTH + 1; /** Maximum length of a FITS string value field. */ public static final int MAX_STRING_VALUE_LENGTH = MAX_VALUE_LENGTH - 2; /** Maximum length of a FITS long string value field. the & for the continuation needs one char. */ public static final int MAX_LONG_STRING_VALUE_LENGTH = MAX_STRING_VALUE_LENGTH - 1; /** if a commend needs the be specified 2 extra chars are needed to start the comment */ public static final int MAX_LONG_STRING_VALUE_WITH_COMMENT_LENGTH = MAX_LONG_STRING_VALUE_LENGTH - 2; /** Maximum HIERARCH keyword length (80 chars must fit [<keyword>=T] at minimum... */ public static final int MAX_HIERARCH_KEYWORD_LENGTH = FITS_HEADER_CARD_SIZE - 2; /** The start and end quotes of the string and the ampasant to continue the string. */ public static final int MAX_LONG_STRING_CONTINUE_OVERHEAD = 3; /** The first ASCII character that may be used in header records */ public static final char MIN_VALID_CHAR = 0x20; /** The last ASCII character that may be used in header records */ public static final char MAX_VALID_CHAR = 0x7e; /** The default keyword to use instead of null or any number of blanks. */ public static final String EMPTY_KEY = ""; /** The string "HIERARCH." */ private static final String HIERARCH_WITH_DOT = NonStandard.HIERARCH.key() + "."; /** The keyword part of the card (set to null if there's no keyword) */ private String key; /** The keyword part of the card (set to null if there's no value / empty string) */ private String value; /** The comment part of the card (set to null if there's no comment) */ private String comment; private IFitsHeader standardKey; /** * The Java class associated to the value * * @since 1.16 */ private Class type; /** * Value type checking policies for when setting values for standardized keywords. * * @author Attila Kovacs * * @since 1.19 */ public enum ValueCheck { /** No value type checking will be performed */ NONE, /** Attempting to set values of the wrong type for standardized keywords will log warnings */ LOGGING, /** Throw exception when setting a value of the wrong type for a standardized keyword */ EXCEPTION } /** * Default value type checking policy for cards with standardized {@link IFitsHeader} keywords. * * @since 1.19 */ public static final ValueCheck DEFAULT_VALUE_CHECK_POLICY = ValueCheck.EXCEPTION; private static ValueCheck valueCheck = DEFAULT_VALUE_CHECK_POLICY; /** Private constructor for an empty card, used by other constructors. */ private HeaderCard() { } /** * Creates a new header card, but reading from the specified data input stream. The card is expected to be describes * by one or more 80-character wide header 'lines'. If long string support is not enabled, then a new card is * created from the next 80-characters. When long string support is enabled, cunsecutive lines starting with * [CONTINUE ] after the first line will be aggregated into a single new card. * * @param dis the data input stream * * @throws UnclosedQuoteException if the line contained an unclosed single quote. * @throws TruncatedFileException if we reached the end of file unexpectedly before fully parsing an 80-character * line. * @throws IOException if there was some IO issue. * * @see FitsFactory#setLongStringsEnabled(boolean) */ @SuppressWarnings("deprecation") public HeaderCard(ArrayDataInput dis) throws UnclosedQuoteException, TruncatedFileException, IOException { this(new HeaderCardCountingArrayDataInput(dis)); } /** * Creates a new header card, but reading from the specified data input. The card is expected to be describes by one * or more 80-character wide header 'lines'. If long string support is not enabled, then a new card is created from * the next 80-characters. When long string support is enabled, cunsecutive lines starting with * [CONTINUE ] after the first line will be aggregated into a single new card. * * @deprecated (for internal use) Its visibility may be reduced or may be removed * entirely in the future. Card counting should be internal to * {@link HeaderCard}. * * @param dis the data input * * @throws UnclosedQuoteException if the line contained an unclosed single quote. * @throws TruncatedFileException if we reached the end of file unexpectedly before fully parsing an * 80-character line. * @throws IOException if there was some IO issue. * * @see #HeaderCard(ArrayDataInput) * @see FitsFactory#setLongStringsEnabled(boolean) */ @Deprecated public HeaderCard(HeaderCardCountingArrayDataInput dis) throws UnclosedQuoteException, TruncatedFileException, IOException { this(); key = null; value = null; comment = null; type = null; String card = readOneHeaderLine(dis); HeaderCardParser parsed = new HeaderCardParser(card); // extract the key key = parsed.getKey(); type = parsed.getInferredType(); if (FitsFactory.isLongStringsEnabled() && parsed.isString() && parsed.getValue().endsWith("&")) { // Potentially a multi-record long string card... parseLongStringCard(dis, parsed); } else { value = parsed.getValue(); type = parsed.getInferredType(); comment = parsed.getTrimmedComment(); } } /** * Creates a new card with a number value. The card will be created either in the integer, fixed-decimal, or format, * with the native precision. If the native precision cannot be fitted in the available card space, the value will * be represented with reduced precision with at least {@link FlexFormat#DOUBLE_DECIMALS}. Trailing zeroes will be * omitted. * * @param key keyword * @param value value (can be null, in which case the card type defaults to * Integer.class) * * @throws HeaderCardException for any invalid keyword or value. * * @since 1.16 * * @see #HeaderCard(String, Number, String) * @see #HeaderCard(String, Number, int, String) * @see #create(IFitsHeader, Number) * @see FitsFactory#setUseExponentD(boolean) */ public HeaderCard(String key, Number value) throws HeaderCardException { this(key, value, FlexFormat.AUTO_PRECISION, null); } /** * Creates a new card with a number value and a comment. The card will be created either in the integer, * fixed-decimal, or format. If the native precision cannot be fitted in the available card space, the value will be * represented with reduced precision with at least {@link FlexFormat#DOUBLE_DECIMALS}. Trailing zeroes will be * omitted. * * @param key keyword * @param value value (can be null, in which case the card type defaults to * Integer.class) * @param comment optional comment, or null * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, Number) * @see #HeaderCard(String, Number, int, String) * @see #create(IFitsHeader, Number) * @see FitsFactory#setUseExponentD(boolean) */ public HeaderCard(String key, Number value, String comment) throws HeaderCardException { this(key, value, FlexFormat.AUTO_PRECISION, comment); } /** * Creates a new card with a number value, using scientific notation, with up to the specified decimal places * showing between the decimal place and the exponent. For example, if decimals is set to 2, then * {@link Math#PI} gets formatted as 3.14E0 (or 3.14D0 if * {@link FitsFactory#setUseExponentD(boolean)} is enabled). * * @param key keyword * @param value value (can be null, in which case the card type defaults to * Integer.class) * @param decimals the number of decimal places to show in the scientific notation. * @param comment optional comment, or null * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, Number) * @see #HeaderCard(String, Number, String) * @see #create(IFitsHeader, Number) * @see FitsFactory#setUseExponentD(boolean) */ public HeaderCard(String key, Number value, int decimals, String comment) throws HeaderCardException { if (value == null) { set(key, null, comment, Integer.class); return; } try { checkNumber(value); } catch (NumberFormatException e) { throw new HeaderCardException("FITS headers may not contain NaN or Infinite values", e); } set(key, new FlexFormat().setWidth(spaceForValue(key)).setPrecision(decimals).format(value), comment, value.getClass()); } /** * Creates a new card with a boolean value (and no comment). * * @param key keyword * @param value value (can be null) * * @throws HeaderCardException for any invalid keyword * * @see #HeaderCard(String, Boolean, String) * @see #create(IFitsHeader, Boolean) */ public HeaderCard(String key, Boolean value) throws HeaderCardException { this(key, value, null); } /** * Creates a new card with a boolean value, and a comment. * * @param key keyword * @param value value (can be null) * @param comment optional comment, or null * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, Boolean) * @see #create(IFitsHeader, Boolean) */ public HeaderCard(String key, Boolean value, String comment) throws HeaderCardException { this(key, value == null ? null : (value ? "T" : "F"), comment, Boolean.class); } /** * Creates a new card with a complex value. The real and imaginary parts will be shown either in the fixed decimal * format or in the exponential notation, whichever preserves more digits, or else whichever is the more compact * notation. Trailing zeroes will be omitted. * * @param key keyword * @param value value (can be null) * * @throws HeaderCardException for any invalid keyword or value. * * @see #HeaderCard(String, ComplexValue, String) * @see #HeaderCard(String, ComplexValue, int, String) */ public HeaderCard(String key, ComplexValue value) throws HeaderCardException { this(key, value, null); } /** * Creates a new card with a complex value and a comment. The real and imaginary parts will be shown either in the * fixed decimal format or in the exponential notation, whichever preserves more digits, or else whichever is the * more compact notation. Trailing zeroes will be omitted. * * @param key keyword * @param value value (can be null) * @param comment optional comment, or null * * @throws HeaderCardException for any invalid keyword or value. * * @see #HeaderCard(String, ComplexValue) * @see #HeaderCard(String, ComplexValue, int, String) */ public HeaderCard(String key, ComplexValue value, String comment) throws HeaderCardException { this(); if (value == null) { set(key, null, comment, ComplexValue.class); return; } if (!value.isFinite()) { throw new HeaderCardException("Cannot represent " + value + " in FITS headers."); } set(key, value.toBoundedString(spaceForValue(key)), comment, ComplexValue.class); } /** * Creates a new card with a complex number value, using scientific (exponential) notation, with up to the specified * number of decimal places showing between the decimal point and the exponent. Trailing zeroes will be omitted. For * example, if decimals is set to 2, then (π, 12) gets formatted as (3.14E0,1.2E1). * * @param key keyword * @param value value (can be null) * @param decimals the number of decimal places to show. * @param comment optional comment, or null * * @throws HeaderCardException for any invalid keyword or value. * * @see #HeaderCard(String, ComplexValue) * @see #HeaderCard(String, ComplexValue, String) */ public HeaderCard(String key, ComplexValue value, int decimals, String comment) throws HeaderCardException { this(); if (value == null) { set(key, null, comment, ComplexValue.class); return; } if (!value.isFinite()) { throw new HeaderCardException("Cannot represent " + value + " in FITS headers."); } set(key, value.toString(decimals), comment, ComplexValue.class); } /** *

* This constructor is now DEPRECATED. You should use {@link #HeaderCard(String, String, String)} to create * cards with null strings, or else {@link #createCommentStyleCard(String, String)} to create any * comment-style card, or {@link #createCommentCard(String)} or {@link #createHistoryCard(String)} to create COMMENT * or HISTORY cards. *

*

* Creates a card with a string value or comment. *

* * @param key The key for the comment or nullable field. * @param comment The comment * @param withNullValue If true the new card will be a value stle card with a null string * value. Otherwise it's a comment-style card. * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, String, String) * @see #createCommentStyleCard(String, String) * @see #createCommentCard(String) * @see #createHistoryCard(String) * * @deprecated Use {@link #HeaderCard(String, String, String)}, or * {@link #createCommentStyleCard(String, String)} instead. */ @Deprecated public HeaderCard(String key, String comment, boolean withNullValue) throws HeaderCardException { this(key, null, comment, withNullValue); } /** *

* This constructor is now DEPRECATED. It has always been a poor construct. You should use * {@link #HeaderCard(String, String, String)} to create cards with null strings, or else * {@link #createCommentStyleCard(String, String)} to create any comment-style card, or * {@link #createCommentCard(String)} or {@link #createHistoryCard(String)} to create COMMENT or HISTORY cards. *

* Creates a comment style card. This may be a comment style card in which case the nullable field should be false, * or a value field which has a null value, in which case the nullable field should be true. * * @param key The key for the comment or nullable field. * @param value The value (can be null) * @param comment The comment * @param nullable If true a null value is a valid value. Otherwise, a * null value turns this into a comment-style card. * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, String, String) * @see #createCommentStyleCard(String, String) * @see #createCommentCard(String) * @see #createHistoryCard(String) * * @deprecated Use {@link #HeaderCard(String, String, String)}, or * {@link #createCommentStyleCard(String, String)} instead. */ @Deprecated public HeaderCard(String key, String value, String comment, boolean nullable) throws HeaderCardException { this(key, value, comment, (nullable || value != null) ? String.class : null); } /** * Creates a new card with a string value (and no comment). * * @param key keyword * @param value value * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, String, String) * @see #create(IFitsHeader, String) */ public HeaderCard(String key, String value) throws HeaderCardException { this(key, value, null, String.class); } /** * Creates a new card with a string value, and a comment * * @param key keyword * @param value value * @param comment optional comment, or null * * @throws HeaderCardException for any invalid keyword or value * * @see #HeaderCard(String, String) * @see #create(IFitsHeader, String) */ public HeaderCard(String key, String value, String comment) throws HeaderCardException { this(key, value, comment, String.class); } /** * Creates a new card from its component parts. Use locally only... * * @param key Case-sensitive keyword (can be null for COMMENT) * @param value the serialized value (tailing spaces will be removed) * @param comment an optional comment or null. * @param type The Java class from which the value field was derived, or null if it's a * comment-style card with a null value. * * @throws HeaderCardException for any invalid keyword or value * * @see #set(String, String, String, Class) */ private HeaderCard(String key, String value, String comment, Class type) throws HeaderCardException { set(key, value, comment, type); this.type = type; } /** * Sets all components of the card to the specified values. For internal use only. * * @param aKey Case-sensitive keyword (can be null for an unkeyed comment) * @param aValue the serialized value (tailing spaces will be removed), or null * @param aComment an optional comment or null. * @param aType The Java class from which the value field was derived, or null if it's a * comment-style card. * * @throws HeaderCardException for any invalid keyword or value */ private synchronized void set(String aKey, String aValue, String aComment, Class aType) throws HeaderCardException { // TODO we never call with null type and non-null value internally, so this is dead code here... // if (aType == null && aValue != null) { // throw new HeaderCardException("Null type for value: [" + sanitize(aValue) + "]"); // } type = aType; // AK: Map null and blank keys to BLANKS.key() // This simplifies things as we won't have to check for null keys separately! if ((aKey == null) || aKey.trim().isEmpty()) { aKey = EMPTY_KEY; } if (aKey.isEmpty() && aValue != null) { throw new HeaderCardException("Blank or null key for value: [" + sanitize(aValue) + "]"); } try { validateKey(aKey); } catch (RuntimeException e) { throw new HeaderCardException("Invalid FITS keyword: [" + sanitize(aKey) + "]", e); } key = aKey; try { validateChars(aComment); } catch (IllegalArgumentException e) { throw new HeaderCardException("Invalid FITS comment: [" + sanitize(aComment) + "]", e); } comment = aComment; try { validateChars(aValue); } catch (IllegalArgumentException e) { throw new HeaderCardException("Invalid FITS value: [" + sanitize(aValue) + "]", e); } if (aValue == null) { value = null; return; } if (isStringValue()) { try { setValue(aValue); } catch (Exception e) { throw new HeaderCardException("Value too long: [" + sanitize(aValue) + "]", e); } } else { aValue = aValue.trim(); // Check that the value fits in the space available for it. if (aValue.length() > spaceForValue()) { throw new HeaderCardException("Value too long: [" + sanitize(aValue) + "]", new LongValueException(key, spaceForValue())); } value = aValue; } } @Override protected HeaderCard clone() { try { return (HeaderCard) super.clone(); } catch (CloneNotSupportedException e) { return null; } } /** * Returns the number of 80-character header lines needed to store the data from this card. * * @return the size of the card in blocks of 80 bytes. So normally every card will return 1. only long stings can * return more than one, provided support for long string is enabled. */ public synchronized int cardSize() { if (FitsFactory.isLongStringsEnabled() && isStringValue() && value != null) { // this is very bad for performance but it is to difficult to // keep the cardSize and the toString compatible at all times return toString().length() / FITS_HEADER_CARD_SIZE; } return 1; } /** * Returns an independent copy of this card. Both this card and the returned value will have identical content, but * modifying one is guaranteed to not affect the other. * * @return a copy of this carf. */ public HeaderCard copy() { HeaderCard copy = clone(); return copy; } /** * Returns the keyword component of this card, which may be empty but never null, but it may be an * empty string. * * @return the keyword from this card, guaranteed to be not null). * * @see #getValue() * @see #getComment() */ @Override public final synchronized String getKey() { return key; } /** * Returns the serialized value component of this card, which may be null. * * @return the value from this card * * @see #getValue(Class, Object) * @see #getKey() * @see #getComment() */ public final synchronized String getValue() { return value; } /** * Returns the comment component of this card, which may be null. * * @return the comment from this card * * @see #getKey() * @see #getValue() */ public final synchronized String getComment() { return comment; } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. * * @return the value from this card * * @throws NumberFormatException if the card's value is null or cannot be parsed as a hexadecimal value. * * @see #getValue() */ @Deprecated public final synchronized long getHexValue() throws NumberFormatException { if (value == null) { throw new NumberFormatException("Card has a null value"); } return Long.decode("0x" + value); } /** *

* Returns the value cast to the specified type, if possible, or the specified default value if the value is * null or if the value is incompatible with the requested type. *

*

* For number types and values, if the requested type has lesser range or precision than the number stored in the * FITS header, the value is automatically downcast (i.e. possible rounded and/or truncated) -- the same as if an * explicit cast were used in Java. As long as the header value is a proper decimal value, it will be returned as * any requested number type. *

* * @param asType the requested class of the value * @param defaultValue the value to use if the card has a null value, or a value that cannot be cast to * the specified type. * @param the generic type of the requested class * * @return the value from this card as a specific type, or the specified default value * * @throws IllegalArgumentException if the specified Java type of not one that is supported for use in FITS headers. */ public synchronized T getValue(Class asType, T defaultValue) throws IllegalArgumentException { if (value == null) { return defaultValue; } if (String.class.isAssignableFrom(asType)) { return asType.cast(value); } if (value.isEmpty()) { return defaultValue; } if (Boolean.class.isAssignableFrom(asType)) { return asType.cast(getBooleanValue((Boolean) defaultValue)); } if (ComplexValue.class.isAssignableFrom(asType)) { return asType.cast(new ComplexValue(value)); } if (Number.class.isAssignableFrom(asType)) { try { BigDecimal big = new BigDecimal(value.toUpperCase().replace('D', 'E')); if (Byte.class.isAssignableFrom(asType)) { return asType.cast(big.byteValue()); } if (Short.class.isAssignableFrom(asType)) { return asType.cast(big.shortValue()); } if (Integer.class.isAssignableFrom(asType)) { return asType.cast(big.intValue()); } if (Long.class.isAssignableFrom(asType)) { return asType.cast(big.longValue()); } if (Float.class.isAssignableFrom(asType)) { return asType.cast(big.floatValue()); } if (Double.class.isAssignableFrom(asType)) { return asType.cast(big.doubleValue()); } if (BigInteger.class.isAssignableFrom(asType)) { return asType.cast(big.toBigInteger()); } // All possibilities have been exhausted, it must be a BigDecimal... return asType.cast(big); } catch (NumberFormatException e) { // The value is not a decimal number, so return the default value by contract. return defaultValue; } } throw new IllegalArgumentException("unsupported class " + asType); } /** * Checks if this card has both a valid keyword and a (non-null) value. * * @return Is this a key/value card? * * @see #isCommentStyleCard() */ public synchronized boolean isKeyValuePair() { return !isCommentStyleCard() && !(key.isEmpty() || value == null); } /** * Checks if this card has a string value (which may be null). * * @return true if this card has a string value, otherwise false. * * @see #isDecimalType() * @see #isIntegerType() * @see #valueType() */ public synchronized boolean isStringValue() { if (type == null) { return false; } return String.class.isAssignableFrom(type); } /** * Checks if this card has a decimal (floating-point) type value (which may be null). * * @return true if this card has a decimal (not integer) type number value, otherwise * false. * * @see #isIntegerType() * @see #isStringValue() * @see #valueType() * * @since 1.16 */ public synchronized boolean isDecimalType() { if (type == null) { return false; } return Float.class.isAssignableFrom(type) || Double.class.isAssignableFrom(type) || BigDecimal.class.isAssignableFrom(type); } /** * Checks if this card has an integer type value (which may be null). * * @return true if this card has an integer type value, otherwise false. * * @see #isDecimalType() * @see #isStringValue() * @see #valueType() * * @since 1.16 */ public synchronized boolean isIntegerType() { if (type == null) { return false; } return Number.class.isAssignableFrom(type) && !isDecimalType(); } /** * Checks if this card is a comment-style card with no associated value field. * * @return true if this card is a comment-style card, otherwise false. * * @see #isKeyValuePair() * @see #isStringValue() * @see #valueType() * * @since 1.16 */ public final synchronized boolean isCommentStyleCard() { return (type == null); } /** * Checks if this card cas a hierarch style long keyword. * * @return true if the card has a non-standard HIERARCH style long keyword, with dot-separated * components. Otherwise false. * * @since 1.16 */ public final synchronized boolean hasHierarchKey() { return isHierarchKey(key); } /** * Sets a new comment component for this card. The specified comment string will be sanitized to ensure it onlly * contains characters suitable for FITS headers. Invalid characters will be replaced with '?'. * * @param comment the new comment text. */ public synchronized void setComment(String comment) { this.comment = sanitize(comment); } /** * Sets a new number value for this card. The new value will be shown in the integer, fixed-decimal, or format, * whichever preserves more digits, or else whichever is the more compact notation. Trailing zeroes will be omitted. * * @param update the new value to set (can be null, in which case the card type * defaults to Integer.class) * * @return the card itself * * @throws NumberFormatException if the input value is NaN or Infinity. * @throws LongValueException if the decimal value cannot be represented in the alotted space * * @see #setValue(Number, int) */ public final HeaderCard setValue(Number update) throws NumberFormatException, LongValueException { return setValue(update, FlexFormat.AUTO_PRECISION); } /** * Sets a new number value for this card, using scientific (exponential) notation, with up to the specified decimal * places showing between the decimal point and the exponent. For example, if decimals is set to 2, * then π gets formatted as 3.14E0. * * @param update the new value to set (can be null, in which case the card type * defaults to Integer.class) * @param decimals the number of decimal places to show in the scientific notation. * * @return the card itself * * @throws NumberFormatException if the input value is NaN or Infinity. * @throws LongValueException if the decimal value cannot be represented in the alotted space * * @see #setValue(Number) */ public synchronized HeaderCard setValue(Number update, int decimals) throws NumberFormatException, LongValueException { if (update instanceof Float || update instanceof Double || update instanceof BigDecimal || update instanceof BigInteger) { checkValueType(IFitsHeader.VALUE.REAL); } else { checkValueType(IFitsHeader.VALUE.INTEGER); } if (update == null) { value = null; type = Integer.class; } else { type = update.getClass(); checkNumber(update); setUnquotedValue(new FlexFormat().forCard(this).setPrecision(decimals).format(update)); } return this; } private static void checkKeyword(IFitsHeader keyword) throws IllegalArgumentException { if (keyword.key().contains("n")) { throw new IllegalArgumentException("Keyword " + keyword.key() + " has unfilled index(es)"); } } private void checkValueType(IFitsHeader.VALUE valueType) throws ValueTypeException { if (standardKey != null) { checkValueType(key, standardKey.valueType(), valueType); } } private static void checkValueType(String key, IFitsHeader.VALUE expect, IFitsHeader.VALUE valueType) throws ValueTypeException { if (expect == IFitsHeader.VALUE.ANY || valueCheck == ValueCheck.NONE) { return; } if (valueType != expect) { if (expect == IFitsHeader.VALUE.REAL && valueType == IFitsHeader.VALUE.INTEGER) { return; } ValueTypeException e = new ValueTypeException(key, valueType.name()); if (valueCheck == ValueCheck.LOGGING) { LOG.warning(e.getMessage()); } else { throw e; } } } /** * Sets a new boolean value for this cardvalueType * * @param update the new value to se (can be null). * * @throws LongValueException if the card has no room even for the single-character 'T' or 'F'. This can never * happen with cards created programmatically as they will not allow setting * HIERARCH-style keywords long enough to ever trigger this condition. But, it is * possible to read cards from a non-standard header, which breaches this limit, by * ommitting some required spaces (esp. after the '='), and have a null value. When * that happens, we can be left without room for even a single character. * @throws ValueTypeException if the card's standard keyword does not support boolean values. * * @return the card itself */ public synchronized HeaderCard setValue(Boolean update) throws LongValueException, ValueTypeException { checkValueType(IFitsHeader.VALUE.LOGICAL); if (update == null) { value = null; } else if (spaceForValue() < 1) { throw new LongValueException(key, spaceForValue()); } else { // There is always room for a boolean value. :-) value = update ? "T" : "F"; } type = Boolean.class; return this; } /** * Sets a new complex number value for this card. The real and imaginary part will be shown in the integer, * fixed-decimal, or format, whichever preserves more digits, or else whichever is the more compact notation. * Trailing zeroes will be omitted. * * @param update the new value to set (can be null) * * @return the card itself * * @throws NumberFormatException if the input value is NaN or Infinity. * @throws LongValueException if the decimal value cannot be represented in the alotted space * * @see #setValue(ComplexValue, int) * * @since 1.16 */ public final HeaderCard setValue(ComplexValue update) throws NumberFormatException, LongValueException { return setValue(update, FlexFormat.AUTO_PRECISION); } /** * Sets a new complex number value for this card, using scientific (exponential) notation, with up to the specified * number of decimal places showing between the decimal point and the exponent. Trailing zeroes will be omitted. For * example, if decimals is set to 2, then (π, 12) gets formatted as (3.14E0,1.2E1). * * @param update the new value to set (can be null) * @param decimals the number of decimal places to show in the scientific notation. * * @return the HeaderCard itself * * @throws NumberFormatException if the input value is NaN or Infinity. * @throws LongValueException if the decimal value cannot be represented in the alotted space * * @see #setValue(ComplexValue) * * @since 1.16 */ public synchronized HeaderCard setValue(ComplexValue update, int decimals) throws LongValueException { checkValueType(IFitsHeader.VALUE.COMPLEX); if (update == null) { value = null; } else { if (!update.isFinite()) { throw new NumberFormatException("Cannot represent " + update + " in FITS headers."); } setUnquotedValue(update.toString(decimals)); } type = ComplexValue.class; return this; } /** * Sets a new unquoted value for this card, checking to make sure it fits in the available header space. If the * value is too long to fit, an IllegalArgumentException will be thrown. * * @param update the new unquoted header value for this card, as a string. * * @throws LongValueException if the value is too long to fit in the available space. */ private synchronized void setUnquotedValue(String update) throws LongValueException { if (update.length() > spaceForValue()) { throw new LongValueException(spaceForValue(), key, value); } value = update; } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. * * @param update the new value to set * * @return the HeaderCard itself * * @throws LongValueException if the value is too long to fit in the available space. * * @since 1.16 */ @Deprecated public synchronized HeaderCard setHexValue(long update) throws LongValueException { setUnquotedValue(Long.toHexString(update)); type = (update == (int) update) ? Integer.class : Long.class; return this; } /** * Sets a new string value for this card. * * @param update the new value to set * * @return the HeaderCard itself * * @throws ValueTypeException if the card's keyword does not support string values. * @throws IllegalStateException if the card has a HIERARCH keyword that is too long to fit any string * value. * @throws IllegalArgumentException if the new value contains characters that cannot be added to the the FITS * header. * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is * currently disabled. * * @see FitsFactory#setLongStringsEnabled(boolean) * @see #validateChars(String) */ public synchronized HeaderCard setValue(String update) throws ValueTypeException, IllegalStateException, IllegalArgumentException, LongStringsNotEnabledException { checkValueType(IFitsHeader.VALUE.STRING); int space = spaceForValue(key); if (space < STRING_QUOTES_LENGTH) { throw new IllegalStateException("No space for string value for [" + key + "]"); } if (update == null) { // There is always room for a null string... value = null; } else { validateChars(update); update = trimEnd(update); int l = getHeaderValueSize(update); if (space < l) { if (FitsFactory.isLongStringsEnabled()) { throw new IllegalStateException("No space for long string value for [" + key + "]"); } throw new LongStringsNotEnabledException("New string value for [" + key + "] is too long." + "\n\n --> You can enable long string support by FitsFactory.setLongStringEnabled(true).\n"); } value = update; } type = String.class; return this; } /** * Returns the modulo 80 character card image, the toString tries to preserve as much as possible of the comment * value by reducing the alignment of the Strings if the comment is longer and if longString is enabled the string * can be split into one more card to have more space for the comment. * * @return the FITS card as one or more 80-character string blocks. * * @throws LongValueException if the card has a long string value that is too long to contain in the * space available after the keyword. * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is * currently disabled. * @throws HierarchNotEnabledException if the card contains a HIERARCH-style long keyword but support for these * is currently disabled. * * @see FitsFactory#setLongStringsEnabled(boolean) */ @Override public String toString() throws LongValueException, LongStringsNotEnabledException, HierarchNotEnabledException { return toString(FitsFactory.current()); } /** * Same as {@link #toString()} just with a prefetched settings object * * @param settings the settings to use for writing the header card * * @return the string representing the card. * * @throws LongValueException if the card has a long string value that is too long to contain in the * space available after the keyword. * @throws LongStringsNotEnabledException if the card contains a long string but support for long strings is * disabled in the settings. * @throws HierarchNotEnabledException if the card contains a HIERARCH-style long keyword but support for these * is disabled in the settings. * * @see FitsFactory#setLongStringsEnabled(boolean) */ protected synchronized String toString(final FitsSettings settings) throws LongValueException, LongStringsNotEnabledException, HierarchNotEnabledException { return new HeaderCardFormatter(settings).toString(this); } /** * Returns the class of the associated value, or null if it's a comment-style card. * * @return the type of the value. * * @see #isCommentStyleCard() * @see #isKeyValuePair() * @see #isIntegerType() * @see #isDecimalType() */ public synchronized Class valueType() { return type; } /** * Returns the value as a boolean, or the default value if the card has no associated value or it is not a boolean. * * @param defaultValue the default value to return if the card has no associated value or is not a boolean. * * @return the boolean value of this card, or else the default value. */ private Boolean getBooleanValue(Boolean defaultValue) { if ("T".equals(value)) { return true; } if ("F".equals(value)) { return false; } return defaultValue; } /** * Parses a continued long string value and comment for this card, which may occupy one or more consecutive * 80-character header records. * * @param dis the input stream from which to parse the value and comment fields of this card. * @param next the parser to use for each 80-character record. * * @throws IOException if there was an IO error reading the stream. * @throws TruncatedFileException if the stream endedc ubnexpectedly in the middle of an 80-character record. */ @SuppressWarnings("deprecation") private synchronized void parseLongStringCard(HeaderCardCountingArrayDataInput dis, HeaderCardParser next) throws IOException, TruncatedFileException { StringBuilder longValue = new StringBuilder(); StringBuilder longComment = null; while (next != null) { if (!next.isString()) { break; } String valuePart = next.getValue(); String untrimmedComment = next.getUntrimmedComment(); if (valuePart == null) { // The card cannot have a null value. If it does it wasn't a string card... break; } // The end point of the value int valueEnd = valuePart.length(); // Check if there card continues into the next record. The value // must end with '&' and the next card must be a CONTINUE card. // If so, remove the '&' from the value part, and parse in the next // card for the next iteration... if (!dis.markSupported()) { throw new IOException("InputStream does not support mark/reset"); } // Peek at the next card. dis.mark(); try { // Check if we should continue parsing this card... next = new HeaderCardParser(readOneHeaderLine(dis)); if (valuePart.endsWith("&") && CONTINUE.key().equals(next.getKey())) { // Remove '& from the value part... valueEnd--; } else { // ok move the input stream one card back. dis.reset(); // Clear the parser also. next = null; } } catch (EOFException e) { // Nothing left to parse after the current one... next = null; } // Append the value part from the record last parsed. longValue.append(valuePart, 0, valueEnd); // Append any comment from the card last parsed. if (untrimmedComment != null) { if (longComment == null) { longComment = new StringBuilder(untrimmedComment); } else { longComment.append(untrimmedComment); } } } comment = longComment == null ? null : longComment.toString().trim(); value = trimEnd(longValue.toString()); type = String.class; } /** * Removes the trailing spaces (if any) from a string. According to the FITS standard, trailing spaces in string are * not significant (but leading spaces are). As such we should remove trailing spaces when parsing header string * values. * * @param s the string as it appears in the FITS header * * @return the input string if it has no trailing spaces, or else a new string with the trailing spaces removed. */ private String trimEnd(String s) { int end = s.length(); for (; end > 0; end--) { if (!Character.isSpaceChar(s.charAt(end - 1))) { break; } } return end == s.length() ? s : s.substring(0, end); } /** * Returns the minimum number of characters the value field will occupy in the header record, including quotes * around string values, and quoted quotes inside. The actual header may add padding (e.g. to ensure the end quote * does not come before byte 20). * * @return the minimum number of bytes needed to represent this value in a header record. * * @since 1.16 * * @see #getHeaderValueSize(String) * @see #spaceForValue() */ synchronized int getHeaderValueSize() { return getHeaderValueSize(value); } /** * Returns the minimum number of characters the value field will occupy in the header record, including quotes * around string values, and quoted quotes inside. The actual header may add padding (e.g. to ensure the end quote * does not come before byte 20). If the long string convention is enabled, this method returns the minimum number * of characters needed in the leading 80-character record only. The call assumes that the value has been * appropriately trimmed of trailing and leading spaces as appropriate. * * @param aValue The proposed value for this card * * @return the minimum number of bytes needed to represent this value in a header record. * * @since 1.16 * * @see #spaceForValue() * @see #trimEnd(String) */ private synchronized int getHeaderValueSize(String aValue) { if (aValue == null) { return 0; } if (!isStringValue()) { return aValue.length(); } int n = STRING_QUOTES_LENGTH; if (FitsFactory.isLongStringsEnabled()) { // If not empty string we need to write at least &... return aValue.isEmpty() ? n : n + 1; } n += aValue.length(); for (int i = aValue.length(); --i >= 0;) { if (aValue.charAt(i) == '\'') { // Add the number of quotes that need escaping. n++; } } return n; } /** * Returns the space available for value and/or comment in a single record the keyword. * * @return the number of characters available in a single 80-character header record for a standard (non long * string) value and/or comment. * * @since 1.16 */ public final synchronized int spaceForValue() { return spaceForValue(key); } /** * Updates the keyword for this card. * * @param newKey the new FITS header keyword to use for this card. * * @throws HierarchNotEnabledException if the new key is a HIERARCH-style long key but support for these is not * currently enabled. * @throws IllegalArgumentException if the keyword contains invalid characters * @throws LongValueException if the new keyword does not leave sufficient room for the current * non-string value. * @throws LongStringsNotEnabledException if the new keyword does not leave sufficient rooom for the current string * value without enabling long string support. * * @see FitsFactory#setLongStringsEnabled(boolean) * @see #spaceForValue() * @see #getValue() */ public synchronized void changeKey(String newKey) throws HierarchNotEnabledException, LongValueException, LongStringsNotEnabledException, IllegalArgumentException { validateKey(newKey); int l = getHeaderValueSize(); int space = spaceForValue(newKey); if (l > space) { if (isStringValue() && !FitsFactory.isLongStringsEnabled() && space > STRING_QUOTES_LENGTH) { throw new LongStringsNotEnabledException(newKey); } throw new LongValueException(spaceForValue(newKey), newKey + "= " + value); } key = newKey; standardKey = null; } /** * Checks if the card is blank, that is if it contains only empty spaces. * * @return true if the card contains nothing but blank spaces. */ public synchronized boolean isBlank() { if (!isCommentStyleCard() || !key.isEmpty()) { return false; } if (comment == null) { return true; } return comment.isEmpty(); } /** * Returns the current policy for checking if set values are of the allowed type for cards with standardized * {@link IFitsHeader} keywords. * * @return the current value type checking policy * * @since 1.19 * * @see #setValueCheckingPolicy(ValueCheck) */ public static ValueCheck getValueCheckingPolicy() { return valueCheck; } /** * Sets the policy to used for checking if set values conform to the expected types for cards that use standardized * FITS keywords via the {@link IFitsHeader} interface. * * @param policy the new polict to use for checking value types. * * @see #getValueCheckingPolicy() * @see Header#setKeywordChecking(nom.tam.fits.Header.KeywordCheck) * * @since 1.19 */ public static void setValueCheckingPolicy(ValueCheck policy) { valueCheck = policy; } /** *

* Creates a new FITS header card from a FITS stream representation of it, which is how the key/value and comment * are represented inside the FITS file, normally as an 80-character wide entry. The parsing of header 'lines' * conforms to all FITS standards, and some optional conventions, such as HIERARCH keywords (if * {@link FitsFactory#setUseHierarch(boolean)} is enabled), COMMENT and HISTORY entries, and OGIP 1.0 long CONTINUE * lines (if {@link FitsFactory#setLongStringsEnabled(boolean)} is enabled). *

*

* However, the parsing here is permissive beyond the standards and conventions, and will do its best to support a * wide range of FITS files, which may deviate from the standard in subtle (or no so subtle) ways. *

*

* Here is a brief summary of the rules that guide the parsing of keywords, values, and comment 'fields' from the * single header line: *

*

* A. Keywords *

*
    *
  • The standard FITS keyword is the first 8 characters of the line, or up to an equal [=] character, whichever * comes first, with trailing spaces removed, and always converted to upper-case.
  • *
  • If {@link FitsFactory#setUseHierarch(boolean)} is enabled, structured longer keywords can be composed after a * HIERARCH base key, followed by space (and/or dot ].]) separated parts, up to an equal sign [=]. The * library will represent the same components (including HIERARCH) but separated by single dots [.]. * For example, the header line starting with [HIERARCH SMA OBS TARGET =], will be referred as * [HIERARCH.SMA.OBS.TARGET] withing this library. The keyword parts can be composed of any ASCII * characters except dot [.], white spaces, or equal [=].
  • *
  • By default, all parts of the key are converted to upper-case. Case sensitive HIERARCH keywords can be * retained after enabling * {@link nom.tam.fits.header.hierarch.IHierarchKeyFormatter#setCaseSensitive(boolean)}.
  • *
*

* B. Values *

*

* Values are the part of the header line, that is between the keyword and an optional ending comment. Legal header * values follow the following parse patterns: *

    *
  • Begin with an equal sign [=], or else come after a CONTINUE keyword.
  • *
  • Next can be a quoted value such as 'hello', placed inside two single quotes. Or an unquoted * value, such as 123.
  • *
  • Quoted values must begin with a single quote ['] and and with the next single quote. If there is no end-quote * in the line, it is not considered a string value but rather a comment, unless * {@link FitsFactory#setAllowHeaderRepairs(boolean)} is enabled, in which case the entire remaining line after the * opening quote is assumed to be a malformed value.
  • *
  • Unquoted values end at the fist [/] character, or else go until the line end.
  • *
  • Quoted values have trailing spaces removed, s.t. [' value '] becomes * [ value].
  • *
  • Unquoted values are trimmed, with both leading and trailing spaces removed, e.g. [ 123 ] * becomes [123].
  • *
*

* C. Comments *

*

* The following rules guide the parsing of the values component: *

    *
  • If a value is present (see above), the comment is what comes after it. That is, for quoted values, everything * that follows the closing quote. For unquoted values, it's what comes after the first [/], with the [/] itself * removed.
  • *
  • If a value is not present, then everything following the keyword is considered the comment.
  • *
  • Comments are trimmed, with both leading and trailing spaces removed.
  • *
* * @return a newly created HeaderCard from a FITS card string. * * @param line the card image (typically 80 characters if in a FITS file). * * @throws IllegalArgumentException if the card was malformed, truncated, or if there was an IO error. * * @see FitsFactory#setUseHierarch(boolean) * @see nom.tam.fits.header.hierarch.IHierarchKeyFormatter#setCaseSensitive(boolean) */ public static HeaderCard create(String line) throws IllegalArgumentException { try (ArrayDataInput in = stringToArrayInputStream(line)) { return new HeaderCard(in); } catch (Exception e) { throw new IllegalArgumentException("card not legal", e); } } final IFitsHeader getStandardKey() { return standardKey; } /** * Creates a new card with a standard or conventional keyword and a boolean value, with the default comment * associated with the keyword. Unlike {@link #HeaderCard(String, Boolean)}, this call does not throw an exception, * since the keyword and comment should be valid by design. * * @param key The standard or conventional keyword with its associated default comment. * @param value the boolean value associated to the keyword * * @return A new header card with the speficied standard-style key and comment and the * specified value, or null if the standard key itself is * malformed or illegal. * * @throws IllegalArgumentException if the standard key was ill-defined. * * @since 1.16 */ public static HeaderCard create(IFitsHeader key, Boolean value) throws IllegalArgumentException { checkKeyword(key); try { HeaderCard hc = new HeaderCard(key.key(), (Boolean) null, key.comment()); hc.standardKey = key; hc.setValue(value); return hc; } catch (HeaderCardException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** *

* Creates a new card with a standard or conventional keyword and a number value, with the default comment * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw a hard * {@link HeaderCardException} exception, since the keyword and comment should be valid by design. (A runtime * {@link IllegalArgumentException} may still be thrown in the event that the supplied conventional keywords itself * is ill-defined -- but this should not happen unless something was poorly coded in this library, on in an * extension of it). *

*

* If the value is not compatible with the convention of the keyword, a warning message is logged but no exception * is thrown (at this point). *

* * @param key The standard or conventional keyword with its associated default comment. * @param value the integer value associated to the keyword. * * @return A new header card with the speficied standard-style key and comment and the * specified value. * * @throws IllegalArgumentException if the standard key itself was ill-defined. * * @since 1.16 */ public static HeaderCard create(IFitsHeader key, Number value) throws IllegalArgumentException { checkKeyword(key); try { HeaderCard hc = new HeaderCard(key.key(), (Number) null, key.comment()); hc.standardKey = key; hc.setValue(value); return hc; } catch (HeaderCardException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** * Creates a new card with a standard or conventional keyword and a number value, with the default comment * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw an exception, * since the keyword and comment should be valid by design. * * @param key The standard or conventional keyword with its associated default comment. * @param value the integer value associated to the keyword. * * @return A new header card with the speficied standard-style key and comment and the * specified value. * * @throws IllegalArgumentException if the standard key was ill-defined. * * @since 1.16 */ public static HeaderCard create(IFitsHeader key, ComplexValue value) throws IllegalArgumentException { checkKeyword(key); try { HeaderCard hc = new HeaderCard(key.key(), (ComplexValue) null, key.comment()); hc.standardKey = key; hc.setValue(value); return hc; } catch (HeaderCardException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** * Creates a new card with a standard or conventional keyword and an integer value, with the default comment * associated with the keyword. Unlike {@link #HeaderCard(String, Number)}, this call does not throw a hard * exception, since the keyword and comment sohould be valid by design. The string value however will be checked, * and an appropriate runtime exception is thrown if it cannot be included in a FITS header. * * @param key The standard or conventional keyword with its associated default comment. * @param value the string associated to the keyword. * * @return A new header card with the speficied standard-style key and comment and the * specified value. * * @throws IllegalArgumentException if the string value contains characters that are not allowed in FITS headers, * that is characters outside of the 0x20 thru 0x7E range, or if the standard * key was ill-defined. */ public static HeaderCard create(IFitsHeader key, String value) throws IllegalArgumentException { checkKeyword(key); validateChars(value); try { HeaderCard hc = new HeaderCard(key.key(), (String) null, key.comment()); hc.standardKey = key; hc.setValue(value); return hc; } catch (HeaderCardException e) { throw new IllegalArgumentException(e.getMessage(), e); } } /** * Creates a comment-style card with no associated value field. * * @param key The keyword, or null blank/empty string for an unkeyed comment. * @param comment The comment text. * * @return a new comment-style header card with the specified key and comment text. * * @throws HeaderCardException if the key or value were invalid. * @throws LongValueException if the comment text is longer than the space available in comment-style cards (71 * characters max) * * @see #createUnkeyedCommentCard(String) * @see #createCommentCard(String) * @see #createHistoryCard(String) * @see Header#insertCommentStyle(String, String) * @see Header#insertCommentStyleMultiline(String, String) */ public static HeaderCard createCommentStyleCard(String key, String comment) throws HeaderCardException, LongValueException { if (comment == null) { comment = ""; } else if (comment.length() > MAX_COMMENT_CARD_COMMENT_LENGTH) { throw new LongValueException(MAX_COMMENT_CARD_COMMENT_LENGTH, key, comment); } HeaderCard card = new HeaderCard(); card.set(key, null, comment, null); return card; } /** * Creates a new unkeyed comment card for th FITS header. These are comment-style cards with no associated value * field, and with a blank keyword. They are commonly used to add explanatory notes in the FITS header. Keyed * comments are another alternative... * * @param text a concise descriptive entry (max 71 characters). * * @return a new COMMENT card with the specified key and comment text. * * @throws HeaderCardException if the text contains invalid charaters. * @throws LongValueException if the comment text is longer than the space available in comment-style cards (71 * characters max) * * @see #createCommentCard(String) * @see #createCommentStyleCard(String, String) * @see Header#insertUnkeyedComment(String) */ public static HeaderCard createUnkeyedCommentCard(String text) throws HeaderCardException, LongValueException { return createCommentStyleCard(BLANKS.key(), text); } /** * Creates a new keyed comment card for th FITS header. These are comment-style cards with no associated value * field, and with COMMENT as the keyword. They are commonly used to add explanatory notes in the FITS header. * Unkeyed comments are another alternative... * * @param text a concise descriptive entry (max 71 characters). * * @return a new COMMENT card with the specified key and comment text. * * @throws HeaderCardException if the text contains invalid charaters. * @throws LongValueException if the comment text is longer than the space available in comment-style cards (71 * characters max) * * @see #createUnkeyedCommentCard(String) * @see #createCommentStyleCard(String, String) * @see Header#insertComment(String) */ public static HeaderCard createCommentCard(String text) throws HeaderCardException, LongValueException { return createCommentStyleCard(COMMENT.key(), text); } /** * Creates a new history record for the FITS header. These are comment-style cards with no associated value field, * and with HISTORY as the keyword. They are commonly used to document the sequence operations that were performed * on the data before it arrived to the state represented by the FITS file. The text field for history entries is * limited to 70 characters max per card. However there is no limit to how many such entries are in a FITS header. * * @param text a concise descriptive entry (max 71 characters). * * @return a new HISTORY card with the specified key and comment text. * * @throws HeaderCardException if the text contains invalid charaters. * @throws LongValueException if the comment text is longer than the space available in comment-style cards (71 * characters max) * * @see #createCommentStyleCard(String, String) * @see Header#insertHistory(String) */ public static HeaderCard createHistoryCard(String text) throws HeaderCardException, LongValueException { return createCommentStyleCard(HISTORY.key(), text); } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. * * @param key the keyword * @param value the integer value * * @return A new header card, with the specified integer in hexadecomal representation. * * @throws HeaderCardException if the card is invalid (for example the keyword is not valid). * * @see #createHexValueCard(String, long, String) * @see #getHexValue() * @see Header#getHexValue(String) */ @Deprecated public static HeaderCard createHexValueCard(String key, long value) throws HeaderCardException { return createHexValueCard(key, value, null); } /** * @deprecated Not supported by the FITS standard, so do not use. It was included due to a * misreading of the standard itself. * * @param key the keyword * @param value the integer value * @param comment optional comment, or null. * * @return A new header card, with the specified integer in hexadecomal representation. * * @throws HeaderCardException if the card is invalid (for example the keyword is not valid). * * @see #createHexValueCard(String, long) * @see #getHexValue() * @see Header#getHexValue(String) */ @Deprecated public static HeaderCard createHexValueCard(String key, long value, String comment) throws HeaderCardException { return new HeaderCard(key, Long.toHexString(value), comment, Long.class); } /** * Reads an 80-byte card record from an input. * * @param in The input to read from * * @return The raw, undigested header record as a string. * * @throws IOException if already at the end of file. * @throws TruncatedFileException if there was not a complete record available in the input. */ private static String readRecord(InputReader in) throws IOException, TruncatedFileException { byte[] buffer = new byte[FITS_HEADER_CARD_SIZE]; int got = 0; try { // Read as long as there is more available, even if it comes in a trickle... while (got < buffer.length) { int n = in.read(buffer, got, buffer.length - got); if (n < 0) { break; } got += n; } } catch (EOFException e) { // Just in case read throws EOFException instead of returning -1 by contract. } if (got == 0) { // Nothing left to read. throw new EOFException(); } if (got < buffer.length) { // Got an incomplete header card... throw new TruncatedFileException( "Got only " + got + " of " + buffer.length + " bytes expected for a header card"); } return AsciiFuncs.asciiString(buffer); } /** * Read exactly one complete fits header line from the input. * * @param dis the data input stream to read the line * * @return a string of exactly 80 characters * * @throwa EOFException if already at the end of file. * * @throws TruncatedFileException if there was not a complete line available in the input. * @throws IOException if the input stream could not be read */ @SuppressWarnings({"resource", "deprecation"}) private static String readOneHeaderLine(HeaderCardCountingArrayDataInput dis) throws IOException, TruncatedFileException { String s = readRecord(dis.in()); dis.cardRead(); return s; } /** * Returns the maximum number of characters that can be used for a value field in a single FITS header record (80 * characters wide), after the specified keyword. * * @param key the header keyword, which may be a HIERARCH-style key... * * @return the space available for the value field in a single record, after the keyword, and the assigmnent * sequence (or equivalent blank space). */ private static int spaceForValue(String key) { if (key.length() > MAX_KEYWORD_LENGTH) { IHierarchKeyFormatter fmt = FitsFactory.getHierarchFormater(); int keyLen = Math.max(key.length(), MAX_KEYWORD_LENGTH) + fmt.getExtraSpaceRequired(key); return FITS_HEADER_CARD_SIZE - keyLen - fmt.getMinAssignLength(); } return FITS_HEADER_CARD_SIZE - (Math.max(key.length(), MAX_KEYWORD_LENGTH) + HeaderCardFormatter.getAssignLength()); } private static ArrayDataInput stringToArrayInputStream(String card) { byte[] bytes = AsciiFuncs.getBytes(card); if (bytes.length % FITS_HEADER_CARD_SIZE != 0) { byte[] newBytes = new byte[bytes.length + FITS_HEADER_CARD_SIZE - bytes.length % FITS_HEADER_CARD_SIZE]; System.arraycopy(bytes, 0, newBytes, 0, bytes.length); Arrays.fill(newBytes, bytes.length, newBytes.length, (byte) ' '); bytes = newBytes; } return new FitsInputStream(new ByteArrayInputStream(bytes)); } /** * This method was designed for use internally. It is 'safe' (not save!) in the sense that the runtime exception it * may throw does not need to be caught. * * @param key keyword * @param comment optional comment, or null * @param hasValue does this card have a (null) value field? If true a * null value of type String.class is assumed (for backward * compatibility). * * @return the new HeaderCard * * @throws HeaderCardException if the card could not be created for some reason (noted as the cause). * * @deprecated This was to be used internally only, without public visibility. It will become * unexposed to users in a future release... */ @Deprecated public static HeaderCard saveNewHeaderCard(String key, String comment, boolean hasValue) throws HeaderCardException { return new HeaderCard(key, null, comment, hasValue ? String.class : null); } /** * Checks if the specified keyword is a HIERARCH-style long keyword. * * @param key The keyword to check. * * @return true if the specified key may be a HIERARC-style key, otehrwise false. */ private static boolean isHierarchKey(String key) { return key.toUpperCase().startsWith(HIERARCH_WITH_DOT); } /** * Replaces illegal characters in the string ith '?' to be suitable for FITS header records. According to the FITS * standard, headers may only contain ASCII characters in the range 0x20 and 0x7E (inclusive). * * @param str the input string. * * @return the sanitized string for use in a FITS header, with illegal characters replaced by '?'. * * @see #isValidChar(char) * @see #validateChars(String) */ public static String sanitize(String str) { int nc = str.length(); char[] cbuf = new char[nc]; for (int ic = 0; ic < nc; ic++) { char c = str.charAt(ic); cbuf[ic] = isValidChar(c) ? c : '?'; } return new String(cbuf); } /** * Checks if a character is valid for inclusion in a FITS header record. The FITS standard specifies that only ASCII * characters between 0x20 thru 0x7E may be used in FITS headers. * * @param c the character to check * * @return true if the character is allowed in the FITS header, otherwise false. * * @see #validateChars(String) * @see #sanitize(String) */ public static boolean isValidChar(char c) { return (c >= MIN_VALID_CHAR && c <= MAX_VALID_CHAR); } /** * Checks the specified string for characters that are not allowed in FITS headers, and throws an exception if any * are found. According to the FITS standard, headers may only contain ASCII characters in the range 0x20 and 0x7E * (inclusive). * * @param text the input string * * @throws IllegalArgumentException if the unput string contains any characters that cannot be in a FITS header, * that is characters outside of the 0x20 to 0x7E range. * * @since 1.16 * * @see #isValidChar(char) * @see #sanitize(String) * @see #validateKey(String) */ public static void validateChars(String text) throws IllegalArgumentException { if (text == null) { return; } for (int i = text.length(); --i >= 0;) { char c = text.charAt(i); if (c < MIN_VALID_CHAR) { throw new IllegalArgumentException( "Non-printable character(s), e.g. 0x" + (int) c + ", in [" + sanitize(text) + "]."); } if (c > MAX_VALID_CHAR) { throw new IllegalArgumentException( "Extendeed ASCII character(s) in [" + sanitize(text) + "]. Only 0x20 through 0x7E are allowed."); } } } /** * Checks if the specified string may be used as a FITS header keyword according to the FITS standard and currently * settings for supporting extensions to the standard, such as HIERARCH-style keywords. * * @param key the proposed keyword string * * @throws IllegalArgumentException if the string cannot be used as a FITS keyword with the current settings. The * exception will contain an informative message describing the issue. * * @since 1.16 * * @see #validateChars(String) * @see FitsFactory#setUseHierarch(boolean) */ public static void validateKey(String key) throws IllegalArgumentException { int maxLength = MAX_KEYWORD_LENGTH; if (isHierarchKey(key)) { if (!FitsFactory.getUseHierarch()) { throw new HierarchNotEnabledException(key); } maxLength = MAX_HIERARCH_KEYWORD_LENGTH; validateHierarchComponents(key); } if (key.length() > maxLength) { throw new IllegalArgumentException("Keyword is too long: [" + sanitize(key) + "]"); } // Check the whole key for non-printable, non-standard ASCII for (int i = key.length(); --i >= 0;) { char c = key.charAt(i); if (c < MIN_VALID_CHAR) { throw new IllegalArgumentException( "Keyword contains non-printable character 0x" + (int) c + ": [" + sanitize(key) + "]."); } if (c > MAX_VALID_CHAR) { throw new IllegalArgumentException("Keyword contains extendeed ASCII characters: [" + sanitize(key) + "]. Only 0x20 through 0x7E are allowed."); } } // Check if the first 8 characters conform to strict FITS specification... for (int i = Math.min(MAX_KEYWORD_LENGTH, key.length()); --i >= 0;) { char c = key.charAt(i); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { continue; } if ((c >= '0' && c <= '9') || (c == '-') || (c == '_')) { continue; } throw new IllegalArgumentException( "Keyword [" + sanitize(key) + "] contains invalid characters. Only [A-Z][a-z][0-9][-][_] are allowed."); } } /** * Additional checks the extended components of the HIEARCH key (in bytes 9-77), to make sure they conform to our * own standards of storing hierarch keys as a dot-separated list of components. That is, the keyword must not have * any spaces... * * @param key the HIERARCH keyword to check. * * @throws IllegalArgumentException if the keyword is not a proper dot-separated set of non-empty hierarchical * components */ private static void validateHierarchComponents(String key) throws IllegalArgumentException { for (int i = key.length(); --i >= 0;) { if (Character.isSpaceChar(key.charAt(i))) { throw new IllegalArgumentException( "No spaces allowed in HIERARCH keywords used internally: [" + sanitize(key) + "]."); } } if (key.indexOf("..") >= 0) { throw new IllegalArgumentException("HIERARCH keywords with empty component: [" + sanitize(key) + "]."); } } /** * Checks that a number value is not NaN or Infinite, since FITS does not have a standard for describing those * values in the header. If the value is not suitable for the FITS header, an exception is thrown. * * @param value The number to check * * @throws NumberFormatException if the input value is NaN or infinite. */ private static void checkNumber(Number value) throws NumberFormatException { if (value instanceof Double) { if (!Double.isFinite(value.doubleValue())) { throw new NumberFormatException("Cannot represent " + value + " in FITS headers."); } } else if (value instanceof Float) { if (!Float.isFinite(value.floatValue())) { throw new NumberFormatException("Cannot represent " + value + " in FITS headers."); } } } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HeaderCardBuilder.java000066400000000000000000000167771476377620500252600ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.util.Date; import nom.tam.fits.header.IFitsHeader; /** * Builder pattern implementation for easy readable header card creation. * * @author nir */ public class HeaderCardBuilder { /** * the header to fill. */ private final Header header; /** * the current card to fill. */ private HeaderCard card; /** * the current selected key. */ private IFitsHeader key; /** * scale to use for decimal values. */ private int precision = -1; /** * constructor to the header card builder. * * @param header the header to fill. * @param key the first header card to set. */ protected HeaderCardBuilder(Header header, IFitsHeader key) { this.header = header; card(key); } /** * get the current build card of the builder. * * @return the current card */ public HeaderCard card() { return card; } /** * switch focus to the card with the specified key. If the card does not exist the card will be created when the * value or the comment is set. * * @param newKey the new card to set * * @return this */ public HeaderCardBuilder card(IFitsHeader newKey) { key = newKey; card = header.getCard(key); return this; } /** * set the comment of the current card. If the card does not exist yet the card is created with a null value, if the * card needs a value use the value setter first! * * @param newComment the new comment to set. * * @return this * * @throws HeaderCardException if the card creation failed. */ public HeaderCardBuilder comment(String newComment) throws HeaderCardException { if (card == null) { card = new HeaderCard(key.key(), (String) null, null); header.addLine(card); } card.setComment(newComment); return this; } /** * set the value of the current card.If the card did not exist yet the card will be created. * * @param newValue the new value to set. * * @return this * * @throws HeaderCardException if the card creation failed. */ public HeaderCardBuilder value(boolean newValue) throws HeaderCardException { if (card == null) { card = new HeaderCard(key.key(), newValue, null); header.addLine(card); } else { card.setValue(newValue); } return this; } /** * set the value of the current card. If the card did not exist yet the card will be created. * * @param newValue the new value to set. * * @return this * * @throws HeaderCardException if the card creation failed. */ public HeaderCardBuilder value(Date newValue) throws HeaderCardException { return value(FitsDate.getFitsDateString(newValue)); } /** * Sets a new number value for the current card. If the card did not exist yet the card will be created. * * @param value the new number value to set. * * @return this * * @throws HeaderCardException if the card creation failed. * @throws LongValueException if the number value cannot be represented in the space available for it in the * 80-character wide FITS header record. */ public HeaderCardBuilder value(Number value) throws HeaderCardException, LongValueException { if (card == null) { card = new HeaderCard(key.key(), value, precision, null); header.addLine(card); } else { card.setValue(value, precision); } return this; } /** * set the value of the current card.If the card did not exist yet the card will be created. * * @param newValue the new value to set. * * @return this * * @throws HeaderCardException if the card creation failed. * @throws LongValueException if the number value cannot be represented in the space available for it in the * 80-character wide FITS header record. */ public HeaderCardBuilder value(String newValue) throws HeaderCardException, LongValueException { if (card == null) { card = new HeaderCard(key.key(), newValue, null); header.addLine(card); } else { card.setValue(newValue); } return this; } /** * Sets the number of decimals to show for the following decimal values. This method is now deprecated. Use * {@link #precision(int)} instead. * * @param decimals the new number of decimal places to show. * * @return this * * @deprecated Use {@link #precision(int)} instead. */ @Deprecated public HeaderCardBuilder scale(int decimals) { precision = decimals; return this; } /** * Sets the number of decimals to show for the following decimal values. Trailing zeroes will be ommitted. * * @param decimals the number of decimals to show for the following decimal values. * * @return this * * @since 1.16 */ public HeaderCardBuilder precision(int decimals) { precision = decimals; return this; } /** * This method has been deprecated. Please use {@link #autoPrecision()} instead. * * @return this * * @deprecated Use {@link #autoPrecision()} instead */ @Deprecated public HeaderCardBuilder noScale() { precision = -1; return this; } /** * Use the native precision for the given number type. Trailing zeroes will be ommitted. * * @return this * * @since 1.16 */ public HeaderCardBuilder autoPrecision() { precision = -1; return this; } /** * Returns the FITS header object that this builder is used with * * @return the filled header. */ public Header header() { return header; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HeaderCardCountingArrayDataInput.java000066400000000000000000000111711476377620500302500ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import nom.tam.util.ArrayDataInput; /** *

* A helper class to keep track of the number of physical cards for a logical card. *

* * @deprecated (for internal use) This class should not have public visibility. And really, the counting should * be completely internalized by {@link HeaderCard}. Perhaps remove in a future major release. * * @author Richard van Nieuwenhoven */ @Deprecated public class HeaderCardCountingArrayDataInput { /** * the input stream. */ private final ArrayDataInput input; /** * the number of 80 byte cards read. */ private int physicalCardsRead; private int markedPhysicalCardsRead; /** * Creates a new instance of this class for counting the number of 80-character header records. * * @deprecated (for internal use) Visibility will be reduced to the package level, or will be removed * entirely. * * @param input The input from which we read the header cards. */ protected HeaderCardCountingArrayDataInput(ArrayDataInput input) { this.input = input; } /** * @deprecated (for internal use) Visibility will be reduced to the package level, or will be removed * entirely. * * @return the number of cards realy read form the stream */ protected int getPhysicalCardsRead() { return physicalCardsRead; } /** * @deprecated (for internal use) Visibility will be reduced to the package level, or will be removed * entirely. * * @return the stream to read the cards from */ protected ArrayDataInput in() { return input; } /** * report a readed card. * * @deprecated (for internal use) Visibility will be reduced to the package level, or will be removed * entirely. */ public void cardRead() { physicalCardsRead++; } /** * indicate whether mark/reset functionality is supported * * @deprecated (for internal use) Visibility will be reduced to the package level, or will be removed * entirely. * * @return true iff mark/reset will work */ public boolean markSupported() { return input.markSupported(); } /** * mark the current position in the stream. * * @deprecated (for internal use) Visibility will be reduced to the package level, or will be * removed entirely. * * @throws IOException if the underlaying stream does not allow the mark. */ public void mark() throws IOException { input.mark(HeaderCard.FITS_HEADER_CARD_SIZE); markedPhysicalCardsRead = physicalCardsRead; } /** * reset the stream th the last marked prosition. * * @deprecated (for internal use) Visibility will be reduced to the package level, or will be * removed entirely. * * @throws IOException if the underlaying stream does not allow the mark. */ public void reset() throws IOException { input.reset(); physicalCardsRead = markedPhysicalCardsRead; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HeaderCardException.java000066400000000000000000000046401476377620500256120ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * Exception that is generated when a header record is in violation of the * standard, or encounters some other header card specific issue. * * @author David Glowacki */ public class HeaderCardException extends FitsException { /** * */ private static final long serialVersionUID = 8575246321991786202L; /** * Instantiates this exception with the designated message string. * * @param s * a human readable message that describes what in fact caused * the exception */ public HeaderCardException(String s) { super(s); } /** * Instantiates this exception with the designated message string, when it * was triggered by some other type of exception * * @param s * a human readable message that describes what in fact caused * the exception * @param reason * the original exception (or other throwable) that triggered * this exception. */ public HeaderCardException(String s, Throwable reason) { super(s, reason); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HeaderCardFormatter.java000066400000000000000000000625171476377620500256260ustar00rootroot00000000000000/* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ package nom.tam.fits; import nom.tam.fits.FitsFactory.FitsSettings; import nom.tam.fits.header.hierarch.IHierarchKeyFormatter; import static nom.tam.fits.header.Standard.CONTINUE; /** * Converts {@link HeaderCard}s into one or more 80-character wide FITS header records. It is a replacement for * {@link nom.tam.fits.utilities.FitsLineAppender}, which is still available for external use for backward * compatibility, but is no longer used internally in this library itself. * * @author Attila Kovacs * * @since 1.16 */ class HeaderCardFormatter { /** * The FITS settings to use, such as support for long strings, support for HIERARCH-style cards, or the use of 'D' * for high-precision exponential values. These settings control when and how header cards are represented exactly * in the FITS header. */ private FitsSettings settings; /** The length of two single quotes. */ private static final int QUOTES_LENGTH = 2; /** * Character sequence that comes after a value field, and before the comment string in the header record. While only * a '/' character is really required, we like to add spaces around it for a more pleasing visual of the resulting * header record. The space before the '/' is strongly recommended by the FITS standard. */ private static final String COMMENT_PREFIX = " / "; /** * Long string comments should not add a space after the '/', because we want to preserve spaces in continued long * string comments, hece we start the comment immediately after the '/' to ensure that internal spaces in wrapped * comments remain intact and properly accounted for. The space before the '/' is strongly recommended by the FITS * standard. */ private static final String LONG_COMMENT_PREFIX = " /"; /** * In older FITS standards there was a requirement that a closing quote for string values may not come before byte * 20 (counted from 1) in the header record. To ensure that, strings need to be padded with blank spaces to push the * closing quote out to that position, if necessary. While it is no longer required by the current FITS standard, it * is possible (or even likely) that some existing tools rely on the earlier requirement. Therefore, we will abide * by the requirements of the older standard. (In the future, we may make this requirement optional, and * controllable through the API). */ private static final int MIN_STRING_END = 19; /** whatever fits after "CONTINUE '' /" */ private static final int MAX_LONG_END_COMMENT = 68 - LONG_COMMENT_PREFIX.length(); /** * Instantiates a new header card formatter with the specified FITS settings. * * @param settings the local FITS settings to use by this card formatter. * * @see #HeaderCardFormatter() */ HeaderCardFormatter(FitsSettings settings) { this.settings = settings; } /** * Converts a {@link HeaderCard} to one or more 80-character wide FITS header records following the FITS rules, and * the various conventions that are allowed by the FITS settings with which this card formatter instance was * created. * * @param card the header card object * * @return the correspoinding FITS header snipplet, as one or more 80-character wide * header 'records'. * * @throws HierarchNotEnabledException if the cards is a HIERARCH-style card, but support for HIERARCH keywords * is not enabled in the FITS settings used by this formatter. * @throws LongValueException if the (non-string) value stored in the card cannot fit in the header * record. * @throws LongStringsNotEnabledException if the card contains a string value that cannot fit into a single header * record, and the use of long string is not enabled in the FITS settings * used by this formatter. * * @see FitsFactory#setLongStringsEnabled(boolean) */ String toString(HeaderCard card) throws HierarchNotEnabledException, LongValueException, LongStringsNotEnabledException { StringBuffer buf = new StringBuffer(HeaderCard.FITS_HEADER_CARD_SIZE); appendKey(buf, card); int valueStart = appendValue(buf, card); int valueEnd = buf.length(); appendComment(buf, card); if (!card.isCommentStyleCard()) { // Strings must be left aligned with opening quote in byte 11 (counted from 1) realign(buf, card.isStringValue() ? valueEnd : valueStart, valueEnd); } pad(buf); return HeaderCard.sanitize(new String(buf)); } /** * Adds the FITS keyword to the header record (normally at the beginning). * * @param buf The string buffer in which we are building the header record. * @param card The header card to be formatted. * * @throws HierarchNotEnabledException if the card contains a HIERARCH-style long keyword, but support for these has * not been enabled in the settings used by this formatter. * @throws LongValueException if the HIERARCH keyword is itself too long to fit on the record without * leaving a minimum amount of space for a value. * * @see FitsFactory#setUseHierarch(boolean) */ private void appendKey(StringBuffer buf, HeaderCard card) throws HierarchNotEnabledException, LongValueException { String key = card.getKey(); if (card.hasHierarchKey()) { IHierarchKeyFormatter fmt = settings.getHierarchKeyFormatter(); if (!settings.isUseHierarch()) { throw new HierarchNotEnabledException(key); } key = fmt.toHeaderString(key); // Calculate the space needed after the keyword int need = fmt.getMinAssignLength(); need += card.getHeaderValueSize(); if (key.length() + need > HeaderCard.FITS_HEADER_CARD_SIZE) { throw new LongValueException(key, HeaderCard.FITS_HEADER_CARD_SIZE - need); } } else { // Just to be certain, we'll make sure base keywords are upper-case, if they // were not already. key = key.toUpperCase(); } buf.append(key); padTo(buf, HeaderCard.MAX_KEYWORD_LENGTH); } /** * Adds the FITS value to the header record (normally after the keyword), including the standard "= " assigment * marker in front of it, or the non-standard "=" (without space after) if * {@link FitsFactory#setSkipBlankAfterAssign(boolean)} is set true. * * @param buf The string buffer in which we are building the header record. * @param card The header card to be formatted. * * @return the buffer position at which the appended value starts, or the last * posirtion if a value was not added at all. (This is used for * realigning later...) * * @throws LongValueException if the card contained a non-string value that is too long to fit in the * space available in the current record. * @throws LongStringsNotEnabledException if the card contains a string value that cannot fit into a single header * record, and the use of long string is not enabled in the FITS settings * used by this formatter. */ private int appendValue(StringBuffer buf, HeaderCard card) throws LongValueException, LongStringsNotEnabledException { String value = card.getValue(); if (card.isCommentStyleCard()) { // comment-style card. Nothing to do here... return buf.length(); } if (card.hasHierarchKey()) { // Flexible assignment sequence depending on space... int space = HeaderCard.FITS_HEADER_CARD_SIZE - buf.length(); if (value != null) { space -= value.length(); } if (card.isStringValue()) { space -= QUOTES_LENGTH; } buf.append(settings.getHierarchKeyFormatter().getAssignStringForSpace(space)); } else { // Add assignment sequence "= " buf.append(getAssignString()); } if (value == null) { // 'null' value, nothing more to append. return buf.length(); } int valueStart = buf.length(); if (card.isStringValue()) { int from = appendQuotedValue(buf, card, 0); while (from < value.length()) { pad(buf); buf.append(CONTINUE.key() + " "); from += appendQuotedValue(buf, card, from); } // TODO We prevent the creation of cards with longer values, so the following check is dead code here. // } else if (value.length() > available) { // throw new LongValueException(available, card.getKey(), card.getValue()); } else { append(buf, value, 0); } return valueStart; } /** * Returns the minimum size of a truncated header comment. When truncating header comments we should preserve at * least the first word of the comment string wholly... * * @param card The header card to be formatted. * * @return the length of the first word in the comment string */ private int getMinTruncatedCommentSize(HeaderCard card) { String comment = card.getComment(); // TODO We check for null before calling, so this is dead code here... // if (comment == null) { // return 0; // } int firstWordLength = comment.indexOf(' '); if (firstWordLength < 0) { firstWordLength = comment.length(); } return COMMENT_PREFIX.length() + firstWordLength; } /** * Appends the comment to the header record, or as much of it as possible, but never less than the first word (at * minimum). * * @param buf The string buffer in which we are building the header record. * @param card The header card to be formatted. * * @return true if the comment was fully represented in the record, or false if it * was truncated or fully ommitted. */ private boolean appendComment(StringBuffer buf, HeaderCard card) { String comment = card.getComment(); if ((comment == null) || comment.isEmpty()) { return true; } int available = getAvailable(buf); boolean longCommentOK = FitsFactory.isLongStringsEnabled() && card.isStringValue(); if (!card.isCommentStyleCard() && longCommentOK) { if (COMMENT_PREFIX.length() + card.getComment().length() > available) { // No room for a complete regular comment, but we can do a long string comment... appendLongStringComment(buf, card); return true; } } if (card.isCommentStyleCard()) { // ' ' instead of '= ' available--; } else { // ' / ' available -= COMMENT_PREFIX.length(); if (getMinTruncatedCommentSize(card) > available) { if (!longCommentOK) { return false; } } } if (card.isCommentStyleCard()) { buf.append(' '); } else { buf.append(COMMENT_PREFIX); } if (available >= comment.length()) { buf.append(comment); return true; } buf.append(comment.substring(0, available)); return false; } /** * Realigns the header record (single records only!) for more pleasing visual appearance by adding padding after a * string value, or before a non-string value, as necessary to push the comment field to the alignment position, if * it's possible without truncating the existing record. * * @param buf The string buffer in which we are building the header record. * @param at The position at which to insert padding * @param from The position in the record that is to be pushed to the alignment position. * * @return true if the card was successfully realigned. Otherwise false. */ private boolean realign(StringBuffer buf, int at, int from) { if ((buf.length() >= HeaderCard.FITS_HEADER_CARD_SIZE) || (from >= Header.getCommentAlignPosition())) { // We are beyond the alignment point already... return false; } return realign(buf, at, from, Header.getCommentAlignPosition()); } /** * Realigns the header record (single records only!) for more pleasing visual appearance by adding padding after a * string value, or before a non-string value, as necessary to push the comment field to the specified alignment * position, if it's possible without truncating the existing record * * @param buf The string buffer in which we are building the header record. * @param at The position at which to insert padding * @param from The position in the record that is to be pushed to the alignment position. * @param to The new alignment position. * * @return true if the card was successfully realigned. Otherwise false. */ private boolean realign(StringBuffer buf, int at, int from, int to) { int spaces = to - from; if (spaces > getAvailable(buf)) { // No space left in card to align the the specified position. return false; } StringBuffer sBuf = new StringBuffer(spaces); while (--spaces >= 0) { sBuf.append(' '); } buf.insert(at, sBuf.toString()); return true; } /** * Adds a long string comment. When long strings are enabled, it is possible to fully preserve a comment of any * length after a string value, by wrapping into multiple records with CONTINUE keywords. Crucially, we will want to * do this in a way as to preserve internal spaces within the comment, when wrapped into multiple records. * * @param buf The string buffer in which we are building the header record. * @param card The header card to be formatted. */ private void appendLongStringComment(StringBuffer buf, HeaderCard card) { // We can wrap the comment to our delight, with CONTINUE! int iLast = buf.length() - 1; String comment = card.getComment(); // We need to amend the last string to end with '&' if (getAvailable(buf) >= LONG_COMMENT_PREFIX.length() + comment.length()) { // We can append the entire comment, easy... buf.append(LONG_COMMENT_PREFIX); append(buf, comment, 0); return; } // Add '&' to the end of the string value. // appendQuotedValue() must always leave space for it! buf.setCharAt(iLast, '&'); buf.append("'"); int from = 0; int available = getAvailable(buf); // If there is room for a standard inline comment, then go for it if (available < COMMENT_PREFIX.length()) { // Add a CONTINUE card with an empty string and try again... pad(buf); buf.append(CONTINUE.key() + " ''"); appendComment(buf, card); return; } buf.append(COMMENT_PREFIX); from = append(buf, comment, 0); // Now add records as needed to write the comment fully... while (from < comment.length()) { pad(buf); buf.append(CONTINUE.key() + " "); buf.append((comment.length() >= from + MAX_LONG_END_COMMENT) ? "'&'" : "''"); buf.append(LONG_COMMENT_PREFIX); from += append(buf, comment, from); } } /** * Appends as many characters as possible from a string, starting at the specified string position, into the header * record. * * @param buf The string buffer in which we are building the header record. * @param text The string from which to append characters up to the end of the record. * @param from The starting position in the string * * @return the number of characters deposited into the header record from the string after the starting * position. */ private int append(StringBuffer buf, String text, int from) { int available = getAvailable(buf); int n = Math.min(available, text.length() - from); if (n < 1) { return 0; } for (int i = 0; i < n; i++) { buf.append(text.charAt(from + i)); } return n; } /** * Appends quoted text from the specified string position, until the end of the string is reached, or until the * 80-character header record is full. It replaces quotes in the string with doubled quotes, while making sure that * not unclosed quotes are left and there is space for an '&' character for * * @param buf The string buffer in which we are building the header record. * @param card The header card whose value to quote in the header record. * @param from The starting position in the string. * * @return the number of characters consumed from the string, which may be different from the number of * characters deposited as each single quote in the input string is represented as 2 single quotes * in the record. */ private int appendQuotedValue(StringBuffer buf, HeaderCard card, int from) { // Always leave room for an extra & character at the end... int available = getAvailable(buf) - QUOTES_LENGTH; // If long strings are enabled leave space for '&' at the end. if (FitsFactory.isLongStringsEnabled() && card.getComment() != null) { if (card.getComment().length() > 0) { available--; } } String text = card.getValue(); // TODO We check for null before calling, so this is dead code here... // if (text == null) { // return 0; // } // The the remaining part of the string fits in the space with the // quoted quotes, then it's easy... if (available >= text.length() - from) { String escaped = text.substring(from).replace("'", "''"); if (escaped.length() <= available) { buf.append('\''); buf.append(escaped); // Earlier versions of the FITS standard required that the closing quote // does not come before byte 20. It's no longer required but older tools // may still expect it, so let's conform. This only affects single // record card, but not continued long strings... if (buf.length() < MIN_STRING_END) { padTo(buf, MIN_STRING_END); } buf.append('\''); return text.length() - from; } } if (!FitsFactory.isLongStringsEnabled()) { throw new LongStringsNotEnabledException(card.getKey() + "= " + card.getValue()); } // Now, we definitely need space for '&' at the end... available = getAvailable(buf) - QUOTES_LENGTH - 1; // We need room for an '&' character at the end also... // TODO Again we prevent this ever occuring before we reach this point, so it is dead code... // if (available < 1) { // return 0; // } // Opening quote buf.append("'"); // For counting the characters consumed from the input int consumed = 0; for (int i = 0; i < available; i++, consumed++) { // TODO We already know we cannot show the whole string on one line, so this is dead code... // if (from + i >= text.length()) { // // Reached end of string; // break; // } char c = text.charAt(from + consumed); if (c == '\'') { // Quoted quotes take up 2 spaces... i++; if (i + 1 >= available) { // Otherwise leave the value quote unconsumed. break; } // Only append the quoted quote if there is room for both. buf.append("''"); } else { // Append a non-quote character. buf.append(c); } } // & and Closing quote buf.append("&'"); return consumed; } /** * Adds a specific amount of padding (empty spaces) in the header record. * * @param buf The string buffer in which we are building the header record. * @param n the number of empty spaces to add. */ private void pad(StringBuffer buf, int n) { for (int i = n; --i >= 0;) { buf.append(' '); } } /** * Pads the current header record with empty spaces to up to the end of the 80-character record. * * @param buf The string buffer in which we are building the header record. */ private void pad(StringBuffer buf) { pad(buf, getAvailable(buf)); } /** * Adds padding (empty spaces) in the header record, up to the specified position within the record. * * @param buf The string buffer in which we are building the header record. * @param to The position in the record to which to pad with spaces. */ private void padTo(StringBuffer buf, int to) { for (int pos = buf.length() % HeaderCard.FITS_HEADER_CARD_SIZE; pos < to; pos++) { buf.append(' '); } } /** * Returns the number of characters available for remaining fields in the current record. Empty records will return * 0. * * @param buf The string buffer in which we are building the header record. * * @return the number of characters still available in the currently started 80-character header record. Empty * records will return 0. */ private int getAvailable(StringBuffer buf) { return (HeaderCard.FITS_HEADER_CARD_SIZE - buf.length() % HeaderCard.FITS_HEADER_CARD_SIZE) % HeaderCard.FITS_HEADER_CARD_SIZE; } /** * Returns the assignment string to use between the keyword and the value. The FITS standard requires the * 2-character sequence "= ", but for some reason we allow to skip the required space after the '=' if * {@link FitsFactory#setSkipBlankAfterAssign(boolean)} is set to true... * * @return The character sequence to insert between the keyword and the value. * * @see #getAssignLength() */ @SuppressWarnings("deprecation") static String getAssignString() { return FitsFactory.isSkipBlankAfterAssign() ? "=" : "= "; } /** * Returns the number of characters we use for assignment. Normally, it should be 2 as per FITS standard, but if * {@link FitsFactory#setSkipBlankAfterAssign(boolean)} is set to true, it may be only 1. * * @return The number of characters that should be between the keyword and the value indicating assignment. * * @see #getAssignString() */ @SuppressWarnings("deprecation") static int getAssignLength() { int n = 1; if (!FitsFactory.isSkipBlankAfterAssign()) { n++; } return n; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HeaderCardParser.java000066400000000000000000000565601476377620500251200ustar00rootroot00000000000000/* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ package nom.tam.fits; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Locale; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import nom.tam.util.ComplexValue; import nom.tam.util.FlexFormat; import static nom.tam.fits.header.NonStandard.HIERARCH; import static nom.tam.fits.header.Standard.CONTINUE; /** *

* Converts a single 80-character wide FITS header record into a header card. See {@link HeaderCard#create(String)} for * a description of the rules that guide parsing. *

*

* When parsing header records that violate FITS standards, the violations can be logged or will throw appropriate * excpetions (depending on the severity of the standard violation and whether * {@link FitsFactory#setAllowHeaderRepairs(boolean)} is enabled or not. The logging of violations is disabled by * default, but may be controlled via {@link Header#setParserWarningsEnabled(boolean)}. *

* * @author Attila Kovacs * * @see FitsFactory#setAllowHeaderRepairs(boolean) * @see Header#setParserWarningsEnabled(boolean) */ class HeaderCardParser { private static final Logger LOG = Logger.getLogger(HeaderCardParser.class.getName()); static { // Do not log warnings by default. LOG.setLevel(Level.SEVERE); } /** regexp for IEEE floats */ private static final Pattern DECIMAL_REGEX = Pattern.compile("[+-]?\\d+(\\.\\d*)?([dDeE][+-]?\\d+)?"); /** regexp for complex numbers */ private static final Pattern COMPLEX_REGEX = Pattern .compile("\\(\\s*" + DECIMAL_REGEX + "\\s*,\\s*" + DECIMAL_REGEX + "\\s*\\)"); /** regexp for decimal integers. */ private static final Pattern INT_REGEX = Pattern.compile("[+-]?\\d+"); /** The header line (usually 80-character width), which to parse. */ private String line; /** * the value of the card. (trimmed and standardized with . in HIERARCH) */ private String key = null; /** * the value of the card. (trimmed) */ private String value = null; /** * the comment specified with the value. */ private String comment = null; /** * was the value quoted? */ private Class type = null; /** * The position in the string that right after the last character processed by this parser */ private int parsePos = 0; /** * Instantiates a new parser for a FITS header line. * * @param line a line in the FITS header, normally exactly 80-characters wide (but need not * be). * * @see #getKey() * @see #getValue() * @see #getComment() * @see #isString() * * @throws UnclosedQuoteException if there is a missing end-quote and header repairs aren't allowed. * @throws IllegalArgumentException if the record contained neither a key or a value. * * @see FitsFactory#setAllowHeaderRepairs(boolean) */ HeaderCardParser(String line) throws UnclosedQuoteException, IllegalArgumentException { this.line = line; // TODO HeaderCard never calls this with a null argument, so the check below is dead code here... // if (line == null) { // throw new IllegalArgumentException("Cannot parse null string"); // } parseKey(); parseValue(); parseComment(); } /** * Returns the keyword component of the parsed header line. If the processing of HIERARCH keywords is enabled, it * may be a `HIERARCH` style long key with the components separated by dots (e.g. * `HIERARCH.ORG.SYSTEM.SUBSYS.ELEMENT`). Otherwise, it will be a standard 0--8 character standard uppercase FITS * keyword (including simply `HIERARCH` if {@link FitsFactory#setUseHierarch(boolean)} was set false). * * @return the FITS header keyword for the line. * * @see FitsFactory#setUseHierarch(boolean) */ String getKey() { return key; } /** * Returns the value component of the parsed header line. * * @return the value part of the line or null if the line contained no value. * * @see FitsFactory#setUseHierarch(boolean) */ String getValue() { return value; } /** * Returns the comment component of the parsed header line, with all leading and trailing spaces preserved. * * @return the comment part of the line or null if the line contained no comment. * * @see #getTrimmedComment() */ String getUntrimmedComment() { return comment; } /** * Returns the comment component of the parsed header line, with both leading and trailing spaces removed * * @return the comment part of the line or null if the line contained no comment. * * @see #getUntrimmedComment() */ String getTrimmedComment() { return comment == null ? null : comment.trim(); } /** * Returns whether the line contained a quoted string value. By default, strings with missing end quotes are no * considered string values, but rather as comments. To allow processing lines with missing quotes as string values, * you must set {@link FitsFactory#setAllowHeaderRepairs(boolean)} to true prior to parsing a header * line with the missing end quote. * * @return true if the value was quoted. * * @see FitsFactory#setAllowHeaderRepairs(boolean) */ boolean isString() { if (type == null) { return false; } return String.class.isAssignableFrom(type); } /** *

* Returns the inferred Java class for the value stored in the header record, such as a {@link String} class, a * {@link Boolean} class, an integer type ({@link Integer}, {@link Long}, or {@link BigInteger}) class, a decimal * type ({@link Float}, {@link Double}, or {@link BigDecimal}) class, a {@link ComplexValue} class, or * null. For number types, it returns the 'smallest' type that can be used to represent the string * value. *

*

* Its an inferred type as the true underlying type that was used to create the value is lost. For example, the * value 42 may have been written from any integer type, including byte or * short, but this routine will guess it to be an int ({@link Integer} type. As such, it * may not be equal to {@link HeaderCard#valueType()} from which the record was created, and hence should not be * used for round-trip testing of type equality. *

* * @return the inferred type of the stored serialized (string) value, or null if the value does not * seem to match any of the supported value types. * * @see HeaderCard#valueType() */ Class getInferredType() { return type; } /** * Parses a fits keyword from a card and standardizes it (trim, uppercase, and hierarch with dots). */ private void parseKey() { /* * AK: The parsing of headers should never be stricter that the writing, such that any header written by this * library can be parsed back without errors. (And, if anything, the parsing should be more permissive to allow * reading FITS produced by other libraries, which may be less stringent in their rules). The original * implementation strongly enforced the ESO HIERARCH convention when reading, but not at all for writing. Here * is a tolerant hierarch parser that will read back any hierarch key that was written by this library. The * input FITS can use any space or even '.' to separate the hierarchies, and the hierarchical elements may * contain any ASCII characters other than those used for separating. It is more in line with what we do with * standard keys too. */ // Find the '=' in the line, if any... int iEq = line.indexOf('='); // The stem is in the first 8 characters or what precedes an '=' character // before that. int endStem = (iEq >= 0 && iEq <= HeaderCard.MAX_KEYWORD_LENGTH) ? iEq : HeaderCard.MAX_KEYWORD_LENGTH; endStem = Math.min(line.length(), endStem); String rawStem = line.substring(0, endStem).trim(); // Check for space at the start of the keyword... if (endStem > 0 && !rawStem.isEmpty()) { if (Character.isSpaceChar(line.charAt(0))) { LOG.warning("[" + sanitize(rawStem) + "] Non-standard starting with a space (trimming)."); } } String stem = rawStem.toUpperCase(); if (!stem.equals(rawStem)) { LOG.warning("[" + sanitize(rawStem) + "] Non-standard lower-case letter(s) in base keyword."); } key = stem; parsePos = endStem; // If not using HIERARCH, then be very resilient, and return whatever key the first 8 chars make... // If the line does not have an '=', can only be a simple key // If it's not a HIERARCH keyword, then return the simple key. if (!FitsFactory.getUseHierarch() || (iEq < 0) || !stem.equals(HIERARCH.key())) { return; } // Compose the hierarchical key... StringTokenizer tokens = new StringTokenizer(line.substring(stem.length(), iEq), " \t\r\n."); StringBuilder builder = new StringBuilder(stem); while (tokens.hasMoreTokens()) { String token = tokens.nextToken(); parsePos = line.indexOf(token, parsePos) + token.length(); // Add a . to separate hierarchies builder.append('.'); builder.append(token); } key = builder.toString(); if (HIERARCH.key().equals(key)) { // The key is only HIERARCH, without a hierarchical keyword after it... LOG.warning("HIERARCH base keyword without HIERARCH-style long key after it."); return; } if (!FitsFactory.getHierarchFormater().isCaseSensitive()) { key = key.toUpperCase(Locale.US); } try { HeaderCard.validateKey(key); } catch (IllegalArgumentException e) { LOG.warning(e.getMessage()); } } /** * Advances the parse position to skip any spaces at the current parse position, and returns whether there is * anything left in the line after the spaces... * * @return true if there is more non-space characters in the string, otherwise false */ private boolean skipSpaces() { for (; parsePos < line.length(); parsePos++) { if (!Character.isSpaceChar(line.charAt(parsePos))) { // Line has non-space characters left to parse... return true; } } // nothing left to parse. return false; } /** * Parses the comment components starting from the current parse position. After this call the parse position is set * to the end of the string. The leading '/' (if found) is not included in the comment. */ private void parseComment() { if (!skipSpaces()) { // nothing left to parse. return; } // if no value, then everything is comment from here on... if (value != null) { if (line.charAt(parsePos) == '/') { // Skip the '/' itself, the comment is whatever is after it. parsePos++; } else { // Junk after a string value -- interpret it as the start of the comment... LOG.warning("[" + sanitize(getKey()) + "] Junk after value (included in the comment)."); } } comment = line.substring(parsePos); parsePos = line.length(); try { HeaderCard.validateChars(comment); } catch (IllegalArgumentException e) { LOG.warning("[" + sanitize(getKey()) + "]: " + e.getMessage()); } } /** * Parses the value component from the current parse position. The parse position is advanced to the first character * after the value specification in the line. If the header line does not contain a value component, then the value * field of this object is set to null. * * @throws UnclosedQuoteException if there is a missing end-quote and header repairs aren't allowed. * * @see FitsFactory#setAllowHeaderRepairs(boolean) */ private void parseValue() throws UnclosedQuoteException { if (key.isEmpty() || !skipSpaces()) { // nothing left to parse. return; } if (CONTINUE.key().equals(key)) { parseValueBody(); } else if (line.charAt(parsePos) == '=') { if (parsePos < HeaderCard.MAX_KEYWORD_LENGTH) { LOG.warning("[" + sanitize(key) + "] assigmment before byte " + (HeaderCard.MAX_KEYWORD_LENGTH + 1) + " for key '" + sanitize(key) + "'."); } if (parsePos + 1 >= line.length()) { LOG.warning("[" + sanitize(key) + "] Record ends with '='."); } else if (line.charAt(parsePos + 1) != ' ') { LOG.warning("[" + sanitize(key) + "] Missing required standard space after '='."); } if (parsePos > HeaderCard.MAX_KEYWORD_LENGTH) { // equal sign = after the 9th char -- only supported with hierarch keys... if (!key.startsWith(HIERARCH.key() + ".")) { LOG.warning("[" + sanitize(key) + "] Possibly misplaced '=' (after byte 9)."); // It's not a HIERARCH key return; } } parsePos++; parseValueBody(); } try { HeaderCard.validateChars(value); } catch (IllegalArgumentException e) { LOG.warning("[" + sanitize(getKey()) + "] " + e.getMessage()); } } /** * Parses the value body from the current parse position. The parse position is advanced to the first character * after the value specification in the line. If the header line does not contain a value component, then the value * field of this object is set to null. * * @throws UnclosedQuoteException if there is a missing end-quote and header repairs aren't allowed. * * @see FitsFactory#setAllowHeaderRepairs(boolean) */ private void parseValueBody() throws UnclosedQuoteException { if (!skipSpaces()) { // nothing left to parse. return; } if (isNextQuote()) { // Parse as a string value, or else throw an exception. parseStringValue(); } else { int end = line.indexOf('/', parsePos); if (end < 0) { end = line.length(); } value = line.substring(parsePos, end).trim(); parsePos = end; type = getInferredValueType(key, value); } } /** * Checks if the next character, at the current parse position, is a single quote. * * @return true if the next character on the line exists and is a single quote, otherwise * false. */ private boolean isNextQuote() { if (parsePos >= line.length()) { // nothing left to parse. return false; } return line.charAt(parsePos) == '\''; } /** * Returns the string fom a parsed string value component, with trailing spaces removed. It preserves leading * spaces. * * @param buf the parsed string value. * * @return the string value with trailing spaces removed. */ private static String getNoTrailingSpaceString(StringBuilder buf) { int to = buf.length(); // Remove trailing spaces only! while (--to >= 0) { if (!Character.isSpaceChar(buf.charAt(to))) { break; } } return to < 0 ? "" : buf.substring(0, to + 1); } /** * Parses a quoted string value starting at the current parse position. If successful, the parse position is updated * to after the string. Otherwise, the parse position is advanced only to skip leading spaces starting from the * input position. * * @throws UnclosedQuoteException if there is a missing end-quote and header repairs aren't allowed. * * @see FitsFactory#setAllowHeaderRepairs(boolean) */ private void parseStringValue() throws UnclosedQuoteException { type = String.class; StringBuilder buf = new StringBuilder(HeaderCard.MAX_VALUE_LENGTH); // Build the string value, up to the end quote and paying attention to double // quotes inside the string, which are translated to single quotes within // the string value itself. for (++parsePos; parsePos < line.length(); parsePos++) { if (isNextQuote()) { parsePos++; if (!isNextQuote()) { // Closing single quote; value = getNoTrailingSpaceString(buf); return; } } buf.append(line.charAt(parsePos)); } // String with missing end quote if (!FitsFactory.isAllowHeaderRepairs()) { throw new UnclosedQuoteException(line); } LOG.warning("[" + sanitize(key) + "] Ignored missing end quote (value parsed to end of record)."); value = getNoTrailingSpaceString(buf); } /** * Returns the inferred Java class for the specified value. See {@link #getInferredType()} for a more detailed * description. * * @param value the serialized (string) representation of a FITS header value. * * @return the inferred type of the specified serialized (string) value, or null if the value * does not seem to match any of the supported value types. null values default to * Boolean.class. */ private static Class getInferredValueType(String key, String value) { // TODO We never call this with null locally, so the following check is dead code here... // if (value == null) { // return Boolean.class; // } if (value.isEmpty()) { LOG.warning("[" + sanitize(key) + "] Null non-string value (defaulted to Boolean.class)."); return Boolean.class; } String trimmedValue = value.trim().toUpperCase(); if ("T".equals(trimmedValue) || "F".equals(trimmedValue)) { return Boolean.class; } if (INT_REGEX.matcher(trimmedValue).matches()) { return getIntegerType(trimmedValue); } if (DECIMAL_REGEX.matcher(trimmedValue).matches()) { return getDecimalType(trimmedValue); } if (COMPLEX_REGEX.matcher(trimmedValue).matches()) { return ComplexValue.class; } LOG.warning("[" + sanitize(key) + "] Unrecognised non-string value type '" + sanitize(trimmedValue) + "'."); return null; } /** * Returns the guessed decimal type of a string representation of a decimal value. * * @param value the string representation of a decimal value. * * @return the The Java class ({@link Float}, {@link Double}, or {@link BigDecimal}) that can be used to * represent the value with the precision provided. * * @see #getInferredValueType() * @see #getIntegerType(String) */ private static Class getDecimalType(String value) { value = value.toUpperCase(Locale.US); boolean hasD = (value.indexOf('D') >= 0); if (hasD) { // Convert the Double Scientific Notation specified by FITS to pure IEEE. value = value.replace('D', 'E'); } BigDecimal big = new BigDecimal(value); // Check for zero, and deal with it separately... if (big.stripTrailingZeros().equals(BigDecimal.ZERO)) { int decimals = big.scale(); if (decimals <= FlexFormat.FLOAT_DECIMALS) { return hasD ? Double.class : Float.class; } if (decimals <= FlexFormat.DOUBLE_DECIMALS) { return Double.class; } return BigDecimal.class; } // Now non-zero values... int decimals = big.precision() - 1; float f = big.floatValue(); if (decimals <= FlexFormat.FLOAT_DECIMALS && (f != 0.0F) && Float.isFinite(f)) { return hasD ? Double.class : Float.class; } double d = big.doubleValue(); if (decimals <= FlexFormat.DOUBLE_DECIMALS && (d != 0.0) && Double.isFinite(d)) { return Double.class; } return BigDecimal.class; } /** * Returns the guessed integer type of a string representation of a integer value. * * @param value the string representation of an integer value. * * @return the The Java class ({@link Integer}, {@link Long}, or {@link BigInteger}) that can be used to * represent the value with the number of digits provided. * * @see #getInferredValueType() * @see #getDecimalType(String) */ private static Class getIntegerType(String value) { int bits = new BigInteger(value).bitLength(); if (bits < Integer.SIZE) { return Integer.class; } if (bits < Long.SIZE) { return Long.class; } return BigInteger.class; } private static String sanitize(String text) { return HeaderCard.sanitize(text); } static Logger getLogger() { return LOG; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HeaderCommentsMap.java000066400000000000000000000064361476377620500253120ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.header.Standard; /** * This class provides a modifiable map in which the comment fields for FITS header keywords produced by this library * are set. The map is a simple String -> String map where the key Strings are normally class:keyword:id where class * is the class name where the keyword is set, keyword is the keyword set and id is an integer used to distinguish * multiple instances. Most users need not worry about this class, but users who wish to customize the appearance of * FITS files may update the map. The code itself is likely to be needed to understand which values in the map must be * modified. * * @deprecated (for internal use) No longer needed */ @Deprecated public final class HeaderCommentsMap { @SuppressWarnings("javadoc") @Deprecated public static void deleteComment(String key) { key = simplyfyKey(key); for (Standard value : Standard.values()) { value.setCommentByKey(key, ""); } } @SuppressWarnings("javadoc") @Deprecated public static String getComment(String key) { key = simplyfyKey(key); for (Standard value : Standard.values()) { String comment = value.getCommentByKey(key); if (comment != null) { return comment; } } return null; } private static String simplyfyKey(String key) { int firstDbPoint = key.indexOf(':'); if (firstDbPoint > 0) { int secondDoublePoint = key.indexOf(':', firstDbPoint + 1); if (secondDoublePoint > 0) { return key.substring(0, secondDoublePoint); } } return key; } @SuppressWarnings("javadoc") @Deprecated public static void updateComment(String key, String comment) { key = simplyfyKey(key); for (Standard value : Standard.values()) { value.setCommentByKey(key, comment); } } private HeaderCommentsMap() { } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HeaderOrder.java000066400000000000000000000123601476377620500241330ustar00rootroot00000000000000package nom.tam.fits; import java.io.Serializable; import java.util.Hashtable; import static nom.tam.fits.header.Standard.BITPIX; import static nom.tam.fits.header.Standard.BLOCKED; import static nom.tam.fits.header.Standard.END; import static nom.tam.fits.header.Standard.EXTEND; import static nom.tam.fits.header.Standard.GCOUNT; import static nom.tam.fits.header.Standard.NAXIS; import static nom.tam.fits.header.Standard.PCOUNT; import static nom.tam.fits.header.Standard.SIMPLE; import static nom.tam.fits.header.Standard.TFIELDS; import static nom.tam.fits.header.Standard.THEAP; import static nom.tam.fits.header.Standard.XTENSION; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * This class implements a comparator which ensures that FITS keywords are written out in a proper order. * * @deprecated (for internal use) Visibility should be reduced to package level in the future */ public class HeaderOrder implements java.util.Comparator, Serializable { /** * */ private static final long serialVersionUID = -5900038332559417655L; /** * This array defines the order of ordered keywords, except END (which we handle separately) */ private static final String[] ORDER = {SIMPLE.key(), XTENSION.key(), BITPIX.key(), NAXIS.key(), PCOUNT.key(), GCOUNT.key(), EXTEND.key(), TFIELDS.key(), BLOCKED.key(), THEAP.key()}; /** * Every keyword is assigned an index. Because NAXIS can have 999 NAXISn variants, we'll space the indices of the * ordered keys by 1000, to allow adding in 999 ordered variants between the major slots. */ private static final int SPACING = 1000; /** * Keys that do not need ordering get an index that comes after the last ordered key, but before END. */ private static final int UNORDERED = SPACING * ORDER.length; /** * The END keyword comes last, so assign it an index after unordered. */ private static final int LAST = UNORDERED + SPACING; /** * Hash table for looking up the index of ordered keys. */ private static final Hashtable LOOKUP = new Hashtable<>(); // Initialize the hash lookup from the order array static { for (int i = 0; i < ORDER.length; i++) { LOOKUP.put(ORDER[i], SPACING * i); } } /** * Returns a virtual ordering index of a given keyword. Keywords with lower indices should precede keywords that * have higher indices. Order does not matter if the indices are the same. * * @param key FITS keyword * * @return The ordering index of that key */ private static int indexOf(String key) { if (key == null) { return UNORDERED; } if (key.startsWith(NAXIS.key())) { if (NAXIS.key().length() == key.length()) { return LOOKUP.get(NAXIS.key()); } try { int i = Integer.parseInt(key.substring(NAXIS.key().length())); if (i < 0 || i >= SPACING) { return UNORDERED; } return LOOKUP.get(NAXIS.key()) + i; } catch (NumberFormatException e) { return UNORDERED; } } if (key.equals(END.key())) { return LAST; } Integer i = LOOKUP.get(key); return i == null ? UNORDERED : i; } /** * Determines the order in which the cards should be added to the header. This method assumes that the arguments are * either the FITS Header keywords as strings, and some other type (or null) for comment style keywords. * * @return -1 if the first argument should be written first
* 1 if the second argument should be written first
* 0 if either is legal. */ @Override public int compare(String c1, String c2) { int i1 = indexOf(c1); int i2 = indexOf(c2); if (i1 == i2) { return 0; } return i1 < i2 ? -1 : 1; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/HierarchNotEnabledException.java000066400000000000000000000042631476377620500273120ustar00rootroot00000000000000/* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ package nom.tam.fits; /** * The keyword is a HIERARCH-style long FITS keyword but the library does not * have the hierarch support enabled at present. * * @author Attila Kovacs * @see FitsFactory#setUseHierarch(boolean) * @since 1.16 */ public class HierarchNotEnabledException extends HeaderCardException { /** * */ private static final long serialVersionUID = 3178787676158092095L; private static String getMessage(String key) { return "Hierarch support is not enabled for [" + key + "]" + "\n\n --> Try FitsFactory.setUseHierarch(true).\n"; } /** * Instantiates a new exception for a HIERARCH-style header keyword, when * support for the hierarch convention is not enabled. * * @param key * the HIERARCH-style FITS keyword. */ public HierarchNotEnabledException(String key) { super(getMessage(key)); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/ImageData.java000066400000000000000000000540621476377620500235700ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.nio.Buffer; import java.util.Arrays; import nom.tam.fits.header.Bitpix; import nom.tam.fits.header.NonStandard; import nom.tam.fits.header.Standard; import nom.tam.image.ImageTiler; import nom.tam.image.StandardImageTiler; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.ArrayFuncs; import nom.tam.util.ComplexValue; import nom.tam.util.Cursor; import nom.tam.util.FitsEncoder; import nom.tam.util.Quantizer; import nom.tam.util.RandomAccess; import nom.tam.util.array.MultiArrayIterator; import nom.tam.util.type.ElementType; /** *

* Image data. Essentially these data are a primitive multi-dimensional array, such as a double[], * float[][], or short[][][]. Or, as of version 1.20, they may also be {@link ComplexValue} * types also. *

*

* Starting in version 0.9 of the FITS library, this class allows users to defer the reading of images if the FITS data * is being read from a file. An {@link ImageTiler} object is supplied which can return an arbitrary subset of the image * as a one dimensional array -- suitable for manipulation by standard Java libraries. The image data may not be read * from the input until the user calls a method that requires the actual data (e.g. the {@link #getData()} / * {@link #getKernel()}, {@link #convertTo(Class)} or {@link #write(ArrayDataOutput)} methods). *

* * @see ImageHDU */ public class ImageData extends Data { private static final String COMPLEX_TYPE = "COMPLEX"; /** * This class describes an array */ protected static class ArrayDesc { private final Class type; private int[] dims; private Quantizer quant; private int complexAxis = -1; ArrayDesc(int[] dims, Class type) { this.dims = dims; this.type = type; if (ComplexValue.class.isAssignableFrom(type)) { complexAxis = dims.length; } } } /** * This inner class allows the ImageTiler to see if the user has read in the data. */ protected class ImageDataTiler extends StandardImageTiler { ImageDataTiler(RandomAccess o, long offset, ArrayDesc d) { super(o, offset, d.dims, d.type); } @Override protected Object getMemoryImage() { return dataArray; } } // private static final Logger LOG = getLogger(ImageData.class); /** The size of the data */ private long byteSize; /** * The actual array of data. This is normally a multi-dimensional primitive array. It may be null until the * getData() routine is invoked, or it may be filled by during the read call when a non-random access device is * used. */ private Object dataArray; /** A description of what the data should look like */ private ArrayDesc dataDescription; /** The image tiler associated with this image. */ private StandardImageTiler tiler; /** * Create the equivalent of a null data element. */ public ImageData() { this(new byte[0]); } /** * (for internal use) Create an array from a header description. This is typically how data will be created * when reading FITS data from a file where the header is read first. This creates an empty array. * * @param h header to be used as a template. * * @throws FitsException if there was a problem with the header description. */ public ImageData(Header h) throws FitsException { dataDescription = parseHeader(h); } /** * Create an ImageData object using the specified object to initialize the data array. * * @param x The initial data array. This should be a primitive array but this is not checked * currently. * * @throws IllegalArgumentException if x is not a suitable primitive array */ public ImageData(Object x) throws IllegalArgumentException { try { checkCompatible(x); } catch (FitsException e) { throw new IllegalArgumentException(e.getMessage(), e); } dataDescription = new ArrayDesc(ArrayFuncs.getDimensions(x), ArrayFuncs.getBaseClass(x)); dataArray = x; byteSize = FitsEncoder.computeSize(x); } @Override protected void loadData(ArrayDataInput in) throws IOException, FitsException { if (tiler != null) { dataArray = tiler.getCompleteImage(); } else { dataArray = ArrayFuncs.newInstance(getType(), getDimensions()); in.readImage(dataArray); } } @Override public void read(ArrayDataInput in) throws FitsException { tiler = (in instanceof RandomAccess) ? new ImageDataTiler((RandomAccess) in, ((RandomAccess) in).getFilePointer(), dataDescription) : null; super.read(in); } @Override protected Object getCurrentData() { return dataArray; } /** * Returns the class that can be used to divide this image into tiles that may be processed separately (and in * parallel). * * @return image tiler for this image instance. */ public StandardImageTiler getTiler() { return tiler; } /** * Sets the buffer that may hold a serialized version of the data for this image. * * @param data the buffer that may hold this image's data in serialized form. */ public void setBuffer(Buffer data) { ElementType elementType = ElementType.forClass(getType()); dataArray = ArrayFuncs.newInstance(getType(), getDimensions()); MultiArrayIterator iterator = new MultiArrayIterator<>(dataArray); Object array = iterator.next(); while (array != null) { elementType.getArray(data, array); array = iterator.next(); } tiler = new ImageDataTiler(null, 0, dataDescription); } @SuppressWarnings({"resource", "deprecation"}) @Override public void write(ArrayDataOutput o) throws FitsException { // Don't need to write null data (noted by Jens Knudstrup) if (byteSize == 0) { return; } if (o != getRandomAccessInput()) { ensureData(); } try { o.writeArray(dataArray); } catch (IOException e) { throw new FitsException("IO Error on image write" + e); } FitsUtil.pad(o, getTrueSize()); } @SuppressWarnings("deprecation") @Override protected void fillHeader(Header head) throws FitsException { if (dataArray == null) { head.nullImage(); return; } Standard.context(ImageData.class); // We'll assume it's a primary image, until we know better... // Just in case, we don't want an XTENSION key lingering around... head.deleteKey(Standard.XTENSION); Cursor c = head.iterator(); c.add(HeaderCard.create(Standard.SIMPLE, true)); Class base = getType(); int[] dims = getDimensions(); if (ComplexValue.class.isAssignableFrom(base)) { dims = Arrays.copyOf(dims, dims.length + 1); dims[dims.length - 1] = 2; base = ComplexValue.Float.class.isAssignableFrom(base) ? float.class : double.class; } c.add(HeaderCard.create(Standard.BITPIX, Bitpix.forPrimitiveType(base).getHeaderValue())); c.add(HeaderCard.create(Standard.NAXIS, dims.length)); for (int i = 1; i <= dims.length; i++) { c.add(HeaderCard.create(Standard.NAXISn.n(i), dims[dims.length - i])); } // Just in case! c.add(HeaderCard.create(Standard.PCOUNT, 0)); c.add(HeaderCard.create(Standard.GCOUNT, 1)); c.add(HeaderCard.create(Standard.EXTEND, true)); if (isComplexValued()) { c.add(HeaderCard.create(Standard.CTYPEn.n(dims.length - dataDescription.complexAxis), COMPLEX_TYPE)); } if (dataDescription.quant != null) { dataDescription.quant.editImageHeader(head); } Standard.context(null); } @Override protected long getTrueSize() { return byteSize; } /** * Returns the image specification based on its description in a FITS header. * * @param h the FITS header that describes this image with the standard keywords for an image HDU. * * @return an object that captures the description contained in the header for internal use. * * @throws FitsException If there was a problem accessing or interpreting the required header values. */ protected ArrayDesc parseHeader(Header h) throws FitsException { String ext = h.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE); if (!ext.equalsIgnoreCase(Standard.XTENSION_IMAGE) && !ext.equalsIgnoreCase(NonStandard.XTENSION_IUEIMAGE)) { throw new FitsException("Not an image header (XTENSION = " + h.getStringValue(Standard.XTENSION) + ")"); } int gCount = h.getIntValue(Standard.GCOUNT, 1); int pCount = h.getIntValue(Standard.PCOUNT, 0); if (gCount > 1 || pCount != 0) { throw new FitsException("Group data treated as images"); } Bitpix bitpix = Bitpix.fromHeader(h); Class baseClass = bitpix.getPrimitiveType(); int ndim = h.getIntValue(Standard.NAXIS, 0); int[] dims = new int[ndim]; // Note that we have to invert the order of the axes // for the FITS file to get the order in the array we // are generating. byteSize = ndim > 0 ? 1 : 0; for (int i = 1; i <= ndim; i++) { int cdim = h.getIntValue(Standard.NAXISn.n(i), 0); if (cdim < 0) { throw new FitsException("Invalid array dimension:" + cdim); } byteSize *= cdim; dims[ndim - i] = cdim; } byteSize *= bitpix.byteSize(); ArrayDesc desc = new ArrayDesc(dims, baseClass); if (COMPLEX_TYPE.equals(h.getStringValue(Standard.CTYPEn.n(1))) && dims[ndim - 1] == 2) { desc.complexAxis = ndim - 1; } else if (COMPLEX_TYPE.equals(h.getStringValue(Standard.CTYPEn.n(ndim))) && dims[0] == 2) { desc.complexAxis = 0; } desc.quant = Quantizer.fromImageHeader(h); if (desc.quant.isDefault()) { desc.quant = null; } return desc; } void setTiler(StandardImageTiler tiler) { this.tiler = tiler; } /** * (for expert users) Overrides the image size description in the header to the specified Java array * dimensions. Typically users should not call this method, unless they want to define the image dimensions in the * absence of the actual complete image data. For example, to describe the dimensions when using low-level writes of * an image row-by-row, without ever storing the entire image in memory. * * @param header A FITS image header * @param sizes The array dimensions in Java order (fastest varying index last) * * @throws FitsException if the size has negative values, or the header is not that for an image * @throws IllegalArgumentException should not actually happen * * @since 1.18 * * @see #fillHeader(Header) */ public static void overrideHeaderAxes(Header header, int... sizes) throws FitsException, IllegalArgumentException { String extType = header.getStringValue(Standard.XTENSION, Standard.XTENSION_IMAGE); if (!extType.equals(Standard.XTENSION_IMAGE) && !extType.equals(NonStandard.XTENSION_IUEIMAGE)) { throw new FitsException("Not an image header (XTENSION = " + extType + ")"); } // Remove prior NAXISn values int n = header.getIntValue(Standard.NAXIS); for (int i = 1; i <= n; i++) { header.deleteKey(Standard.NAXISn.n(i)); } Cursor c = header.iterator(); c.setKey(Standard.NAXIS.key()); c.add(HeaderCard.create(Standard.NAXIS, sizes.length)); for (int i = 1; i <= sizes.length; i++) { int l = sizes[sizes.length - i]; if (l < 0) { throw new FitsException("Invalid size[ " + i + "] = " + l); } c.add(HeaderCard.create(Standard.NAXISn.n(i), l)); } } /** * Creates a new FITS image using the specified primitive numerical Java array containing data. * * @param data A regulatly shaped primitive numerical Java array, which can be * multi-dimensional. * * @return A new FITS image that encapsulates the specified array data. * * @throws IllegalArgumentException if the argument is not a primitive numerical Java array. * * @since 1.19 */ public static ImageData from(Object data) throws IllegalArgumentException { return new ImageData(data); } /** * Checks if a given data object may constitute the kernel for an image. To conform, the data must be a regularly * shaped primitive numerical array of ant dimensions, or null. * * @param data A regularly shaped primitive numerical array of ny dimension, or * null * * @throws IllegalArgumentException If the array is not regularly shaped. * @throws FitsException If the argument is not a primitive numerical array type * * @since 1.19 */ static void checkCompatible(Object data) throws IllegalArgumentException, FitsException { if (data != null) { Class base = ArrayFuncs.getBaseClass(data); if (ComplexValue.Float.class.isAssignableFrom(base)) { base = float.class; } else if (ComplexValue.class.isAssignableFrom(base)) { base = double.class; } Bitpix.forPrimitiveType(base); ArrayFuncs.checkRegularArray(data, false); } } @Override @SuppressWarnings("deprecation") public ImageHDU toHDU() throws FitsException { Header h = new Header(); fillHeader(h); return new ImageHDU(h, this); } /** * Sets the conversion between decimal and integer data representations. The quantizer for the image is set * automatically if the image was read from a FITS input, and if any of the associated BSCALE, BZERO, or BLANK * keywords were defined in the HDU's header. User may use this methods to set a different quantization or to use no * quantization at all when converting between floating-point and integer representations. * * @param quant the quantizer that converts between floating-point and integer data representations, or * null to not use quantization and instead rely on simple rounding for decimal-ineger conversions.. * * @see #getQuantizer() * @see #convertTo(Class) * * @since 1.20 */ public void setQuantizer(Quantizer quant) { dataDescription.quant = quant; } /** * Returns the conversion between decimal and integer data representations. * * @return the quantizer that converts between floating-point and integer data representations, which may be * null * * @see #setQuantizer(Quantizer) * @see #convertTo(Class) * * @since 1.20 */ public final Quantizer getQuantizer() { return dataDescription.quant; } /** * Returns the element type of this image in its current representation. * * @return The element type of this image, such as int.class, double.class or * {@link ComplexValue}.class. * * @see #getDimensions() * @see #isComplexValued() * @see #convertTo(Class) * * @since 1.20 */ public final Class getType() { return dataDescription.type; } /** * Returns the dimensions of this image. * * @return An array containing the sizes along each data dimension, in Java indexing order. The returned array is * not used internally, and therefore modifying it will not damage the integrity of the image data. * * @see #getType() * * @since 1.20 */ public final int[] getDimensions() { return Arrays.copyOf(dataDescription.dims, dataDescription.dims.length); } /** * Checks if the image data is explicitly designated as a complex-valued image. An image may be designated as * complex-valued either because it was created with {@link ComplexValue} type data, or because it was read from a * FITS file in which one image axis of dimension 2 was designated as an axis containing complex-valued components * with the corresponding CTYPEn header keyword set to 'COMPLEX'. The complex-valued deignation checked by this * method is not the same as {@link #getType()}, as it does not necesarily mean that the data itself is currently in * {@link ComplexValue} type representation. Rather it simply means that this data can be represented as * {@link ComplexValue} type, possibly after an appropriate conversion to a {@link ComplexValue} type. * * @return true if the data is complex valued or has been explicitly designated as complex valued. * Otherwise false. * * @see #convertTo(Class) * @see #getType() * * @since 1.20 */ public final boolean isComplexValued() { return dataDescription.complexAxis >= 0; } /** * Converts this image HDU to another image HDU of a different type, possibly using a qunatizer for the * integer-decimal conversion of the data elements. In all other respects, the returned image is identical to the * the original. If th conversion is th indetity, it will return itself and the data may remain in deferred mode. * * @param type The primitive numerical type (e.g. int.class or double.class), or * else a {@link ComplexValue} type in which data should be represented. Complex * representations are normally available for data whose first or last CTYPEn axis was * described as 'COMPLEX' by the FITS header with a dimensionality is 2 corresponfing to a * pair of real and imaginary data elements. Even without the CTYPEn designation, it is * always possible to convert to complex all arrays that have a trailing Java dimension * (NAXIS1 in FITS) equal to 2. * * @return An image HDU containing the same data in the chosen representation by another type. (It may * be the same as this HDU if the type is unchanged from the original). * * @throws FitsException if the data cannot be read from the input. * * @see #isComplexValued() * @see ArrayFuncs#convertArray(Object, Class, Quantizer) * * @since 1.20 */ public ImageData convertTo(Class type) throws FitsException { if (type.isAssignableFrom(getType())) { return this; } ensureData(); ImageData typed = null; boolean toComplex = ComplexValue.class.isAssignableFrom(type) && !ComplexValue.class.isAssignableFrom(getType()); if (toComplex && dataDescription.complexAxis == 0) { // Special case of converting separate re/im arrays to complex... // 1. Convert to intermediate floating-point class as necessary (with quantization if any) Class numType = ComplexValue.Float.class.isAssignableFrom(type) ? float.class : double.class; Object[] t = (Object[]) ArrayFuncs.convertArray(dataArray, numType, getQuantizer()); ImageData f = new ImageData(ArrayFuncs.decimalsToComplex(t[0], t[1])); f.dataDescription.quant = getQuantizer(); // 2. Assemble complex from separate re/im components. return f.convertTo(type); } typed = new ImageData(ArrayFuncs.convertArray(dataArray, type, getQuantizer())); typed.dataDescription.quant = getQuantizer(); return typed; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/ImageHDU.java000066400000000000000000000237511476377620500233400ustar00rootroot00000000000000package nom.tam.fits; import java.io.PrintStream; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.fits.header.Standard; import nom.tam.image.StandardImageTiler; import nom.tam.util.ArrayFuncs; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.fits.header.Standard.BITPIX; import static nom.tam.fits.header.Standard.GROUPS; import static nom.tam.fits.header.Standard.NAXIS; import static nom.tam.fits.header.Standard.NAXISn; import static nom.tam.fits.header.Standard.SIMPLE; import static nom.tam.fits.header.Standard.XTENSION; import static nom.tam.util.LoggerHelper.getLogger; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Header/data unit for images. Image HDUs are suitable for storing monolithic regular numerical arrays in 1 to 255 * dimensions, such as a double[], float[][], or short[][][]. ((FITS supports up * to 999 dimensions, but Java support maxes at at 255 -- however it's unlikely you'll find this to be a serious * limitation.) * * @see ImageData */ @SuppressWarnings("deprecation") public class ImageHDU extends BasicHDU { private static final Logger LOG = getLogger(ImageHDU.class); @Override protected final String getCanonicalXtension() { return Standard.XTENSION_IMAGE; } /** * @deprecated (for internal use) Use {@link ImageData#from(Object)} instead. Will * reduce visibility in the future * * @return Encapsulate an object as an ImageHDU. * * @param o object to encapsulate * * @throws FitsException does not actually throw this exception * @throws IllegalArgumentException if the data is not a regular primitive numerical array suitable for an * image. */ @Deprecated public static ImageData encapsulate(Object o) throws IllegalArgumentException, FitsException { return new ImageData(o); } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return is this object can be described as a FITS image. * * @param o The Object being tested. */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isData(Object o) { try { ImageData.checkCompatible(o); } catch (Exception e) { return false; } return true; } /** * Check that this HDU has a valid header for this type. * * @deprecated (for internal use) Will reduce visibility in the future * * @param hdr header to check * * @return true if this HDU has a valid header. */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isHeader(Header hdr) { boolean found = hdr.getBooleanValue(SIMPLE); if (!found) { String xtension = hdr.getStringValue(XTENSION); xtension = xtension == null ? "" : xtension.trim(); if (Standard.XTENSION_IMAGE.equals(xtension) || "IUEIMAGE".equals(xtension)) { found = true; } } if (!found) { return false; } return !hdr.getBooleanValue(GROUPS); } /** * Prepares a data object into which the actual data can be read from an input subsequently or at a later time. * * @deprecated (for internal use) Will reduce visibility in the future * * @param hdr The FITS header that describes the data * * @return A data object that support reading content from a stream. * * @throws FitsException if the data could not be prepared to prescriotion. */ @Deprecated public static ImageData manufactureData(Header hdr) throws FitsException { return new ImageData(hdr); } /** * Prepares a data object into which the actual data can be read from an input subsequently or at a later time. * * @deprecated (for internal use) Will reduce visibility in the future * * @param d The FITS data content of this HDU * * @return A data object that support reading content from a stream. * * @throws FitsException if the data could not be prepared to prescriotion. */ @Deprecated public static Header manufactureHeader(Data d) throws FitsException { if (d == null) { return null; } Header h = new Header(); d.fillHeader(h); return h; } /** * Build an image HDU using the supplied data. * * @deprecated (for internal use) Its visibility should be reduced to package level in the future. * * @param h the header for the image. * @param d the data used in the image. */ public ImageHDU(Header h, ImageData d) { super(h, d); } /** * Returns the class that can be used to divide this image into tiles that may be processed separately (and in * parallel). * * @return image tiler for this image instance. * * @see ImageData#getTiler() */ public StandardImageTiler getTiler() { return myData.getTiler(); } @Override public void info(PrintStream stream) { if (isHeader(myHeader)) { stream.println(" Image"); } else { stream.println(" Image (bad header)"); } stream.println(" Header Information:"); stream.println(" BITPIX=" + myHeader.getIntValue(BITPIX, -1)); int naxis = myHeader.getIntValue(NAXIS, -1); stream.println(" NAXIS=" + naxis); for (int i = 1; i <= naxis; i++) { stream.println(" NAXIS" + i + "=" + myHeader.getIntValue(NAXISn.n(i), -1)); } stream.println(" Data information:"); try { if (myData.getData() == null) { stream.println(" No Data"); } else { stream.println(" " + ArrayFuncs.arrayDescription(myData.getData())); } } catch (Exception e) { LOG.log(Level.SEVERE, "Unable to get image data", e); stream.println(" Unable to get data"); } } /** * Returns the name of the physical unit in which images are represented. * * @return the standard name of the physical unit in which the image is expressed, e.g. "Jy beam^{-1}". */ @Override public String getBUnit() { return super.getBUnit(); } /** * Returns the integer value that signifies blank (missing or null) data in an integer image. * * @return the integer value used for identifying blank / missing data in integer images. * * @throws FitsException if the header does not specify a blanking value or if it is not appropriate for the type of * imge (that is not an integer type image) */ @Override public long getBlankValue() throws FitsException { if (getBitpix().getHeaderValue() < 0) { throw new FitsException("No integer blanking value in floating-point images."); } return super.getBlankValue(); } /** * Returns the floating-point increment between adjacent integer values in the image. Strictly speaking, only * integer-type images should define a quantization scaling, but there is no harm in having this value in * floating-point images also -- which may be interpreted as a hint for quantization, perhaps. * * @return the floating-point quantum that corresponds to the increment of 1 in the integer data representation. * * @see #getBZero() */ @Override public double getBScale() { return super.getBScale(); } /** * Returns the floating-point value that corresponds to an 0 integer value in the image. Strictly speaking, only * integer-type images should define a quantization offset, but there is no harm in having this value in * floating-point images also -- which may be interpreted as a hint for quantization, perhaps. * * @return the floating point value that correspond to the integer 0 in the image data. * * @see #getBScale() */ @Override public double getBZero() { return super.getBZero(); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/LongStringsNotEnabledException.java000066400000000000000000000044761476377620500300440ustar00rootroot00000000000000/* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ package nom.tam.fits; /** * The string value does not fit into a signle 80-character wide FITS header * record, and the library does not have long string support enabled at present. * * @author Attila Kovacs * @see FitsFactory#setLongStringsEnabled(boolean) * @since 1.16 */ public class LongStringsNotEnabledException extends HeaderCardException { /** * */ private static final long serialVersionUID = -6255591057669953444L; private static String getMessage(String key) { return "Long string support is not enabled for [" + key + "]" + "\n\n --> Try FitsFactory.setLongStringsEnabled(true).\n"; } /** * Instantiates a new exception for when a string value does not fit in a * single 80-character header record, and support for the standard long * string convention has not been enabled. * * @param key * the header keyword for which the exception occurred. */ public LongStringsNotEnabledException(String key) { super(getMessage(key)); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/LongValueException.java000066400000000000000000000075731476377620500255340ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * The value cannot fit into the available header space, as requested. Perhaps * it's preceded by a very long HIEARACH keyword, or else it's a numerical value * that is too long. Either way, there is no way we can represent this value in * its current form (and keyword) in a FITS header. * * @author Attila Kovacs * @since 1.16 */ public class LongValueException extends HeaderCardException { /** * */ private static final long serialVersionUID = -1446259888260331392L; /** * Instantiates a new exception about a long value that cannot be fitted * into the space available for it in the FITS header. * * @param spaceAvailable * the maximum space available for the value field for the given * record. */ public LongValueException(int spaceAvailable) { super("Not enough space (" + spaceAvailable + ") for value"); } /** * Instantiates a new exception about a long value that cannot be fitted * into the space available for it in the FITS header. * * @param key * the header keyword for which the exception occurred. * @param spaceAvailable * the maximum space available for the value field for the given * record. */ public LongValueException(String key, int spaceAvailable) { super("Not enough space (" + spaceAvailable + ") for value of [" + key + "]"); } /** * Instantiates a new exception about a long value that cannot be fitted * into the space available for it in the FITS header. * * @param spaceAvailable * the maximum space available for the value field for the given * record. * @param value * the header value that was too long. */ public LongValueException(int spaceAvailable, String value) { super("Not enough space (" + spaceAvailable + ") for: [" + value + "]"); } /** * Instantiates a new exception about a long value that cannot be fitted * into the space available for it in the FITS header. * * @param spaceAvailable * the maximum space available for the value field for the given * record. * @param key * the header keyword for which the exception occurred. * @param value * the header value that was too long. */ public LongValueException(int spaceAvailable, String key, String value) { super("Not enough space (" + spaceAvailable + ") for: [" + key + "=" + value + "]"); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/NullData.java000066400000000000000000000051701476377620500234540ustar00rootroot00000000000000package nom.tam.fits; import java.nio.Buffer; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.header.Bitpix; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import static nom.tam.fits.header.Standard.EXTEND; import static nom.tam.fits.header.Standard.GCOUNT; import static nom.tam.fits.header.Standard.PCOUNT; /** * A subclass of Data containing no actual data. It wraps an underlying data of null. * * @author Attila Kovacs * * @since 1.18 * * @see NullDataHDU */ public final class NullData extends ImageData { @SuppressWarnings("deprecation") @Override protected void fillHeader(Header head) { head.setSimple(true); head.setBitpix(Bitpix.INTEGER); head.setNaxes(0); // Just in case! head.addValue(EXTEND, true); head.addValue(GCOUNT, 1); head.addValue(PCOUNT, 0); } @Override protected void loadData(ArrayDataInput in) { return; } @Override protected Void getCurrentData() { return null; } @Override protected long getTrueSize() { return 0; } @Override public void read(ArrayDataInput in) { setFileOffset(in); } @Override public void write(ArrayDataOutput o) { } @Override public void setBuffer(Buffer data) { // Nothing to do. } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/NullDataHDU.java000066400000000000000000000046331476377620500240200ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.PrintStream; /** * A class of HDU that contains only a header only with no associated data. Such HDUs are commonly used as the primary * HDU in FITS files where the leading data is not an image, since only images may constitute the primary HDU. * * @author Attila Kovacs * * @since 1.18 * * @see NullData */ @SuppressWarnings("deprecation") public class NullDataHDU extends ImageHDU { /** * Instantiates a new HDU with a default header and no associated data. */ public NullDataHDU() { super(new Header(), new NullData()); getData().fillHeader(getHeader()); } @Override public NullData getData() { return (NullData) super.getData(); } /** * Instantiates a new HDU with only the supplied header and no associated data. * * @param myHeader the FITS header for this HDU */ public NullDataHDU(Header myHeader) { super(myHeader, new NullData()); getData().fillHeader(getHeader()); } @Override public void info(PrintStream stream) { stream.println(" Header Only"); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/PaddingException.java000066400000000000000000000041551476377620500251770ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * This exception is thrown if padding is missing between the end of a FITS data * segment and the end-of-file. This padding is required by the FITS standard, * but some FITS writers may not add it. As of 1.17 our `Fits` class deals * seamlessly with such data, since the missing padding at the end-of-file is * harmless when reading in data. It will log a warning but proceed normally. * However, the exception is still thrown when using low-level * {@link Data#read(nom.tam.util.ArrayDataInput)} to allow expert users to deal * with this issue in any way they see fit. */ public class PaddingException extends FitsException { /** * */ private static final long serialVersionUID = 8716484905278318366L; PaddingException(String msg, Exception cause) { super(msg, cause); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/RandomGroupsData.java000066400000000000000000000223411476377620500251610ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam.fits * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.lang.reflect.Array; import nom.tam.fits.header.Bitpix; import nom.tam.fits.header.Standard; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.ArrayFuncs; import nom.tam.util.Cursor; import nom.tam.util.FitsEncoder; /** * Random Groups data. The use of random groups is discouraged, even by the FITS standard. Some old radio data may be * packaged in this format. Thus apart from provided limited support for reading such data, users should not create * random groups anew. {@link BinaryTable} offers a much more flexible and capable way for storing an ensemble of * parameters, arrays, and more. *

* Random groups are instantiated as a two-dimensional array of objects. The first dimension of the array is the number * of groups. The second dimension is 2. The first object in every row is a one dimensional parameter array. The second * element is the n-dimensional data array. * * @see BinaryTable */ public class RandomGroupsData extends Data { private int groups; private Object[] sampleRow; private Object[][] dataArray; /** * Create the equivalent of a null data element. */ public RandomGroupsData() { dataArray = new Object[0][]; } /** * Constructor used by RandomGroupsHDU only... * * @param gcount The number of parameter groups * @param sampleRow The 2-element array of a sample group. * * @since 1.18 */ RandomGroupsData(int gcount, Object[] sampleRow) { this(); groups = gcount; this.sampleRow = sampleRow; } /** * Create a RandomGroupsData object using the specified object to initialize the data array. * * @param x The initial data array. This should a two-d array of objects as described above. * * @throws IllegalArgumentException if the second array dimension is specified and it is not 2, or if the parameter * arrya is not 1-dimensional, or if the parameter and data types differ. */ public RandomGroupsData(Object[][] x) throws IllegalArgumentException { dataArray = x == null ? new Object[0][] : x; groups = dataArray.length; if (groups > 0) { if (dataArray[0].length != 2) { throw new IllegalArgumentException("Second array dimension must be 2"); } if (Array.getLength(ArrayFuncs.getDimensions(dataArray[0][0])) != 1) { throw new IllegalArgumentException("Expected 1D parameter array."); } if (dataArray[0][1] != null) { Class pbase = ArrayFuncs.getBaseClass(dataArray[0][0]); Class dbase = ArrayFuncs.getBaseClass(dataArray[0][1]); if (pbase != dbase) { throw new IllegalArgumentException( "Mismatched parameters and data types (" + pbase.getName() + " vs " + dbase.getName() + ")"); } } sampleRow = new Object[2]; sampleRow[0] = ArrayFuncs.deepClone(dataArray[0][0]); sampleRow[1] = ArrayFuncs.deepClone(dataArray[0][1]); } } /** * Returns the Java class of the the parameter and data array elements. * * @return The java class of the parameter and data elements. * * @since 1.18 */ public Class getElementType() { return sampleRow == null ? null : ArrayFuncs.getBaseClass(sampleRow[0]); } /** * Returns the dimensions of the grouped parameters * * @return The dimensions of the parameters or -1 if not defined. * * @see #getDataDims() * * @since 1.18 */ public int getParameterCount() { return sampleRow == null ? -1 : Array.getLength(sampleRow[0]); } /** * Returns the dimensions of the grouped data * * @return The dimensions of the parameters, or null if not defined. * * @see #getParameterCount() * * @since 1.18 */ public int[] getDataDims() { return sampleRow == null ? null : ArrayFuncs.getDimensions(sampleRow[1]); } @SuppressWarnings("deprecation") @Override protected void fillHeader(Header h) throws FitsException { if (groups <= 0) { throw new FitsException("Invalid (empty) random group data"); } Standard.context(RandomGroupsData.class); // We'll assume it's a primary image, until we know better... // Just in case, we don't want an XTENSION key lingering around... h.deleteKey(Standard.XTENSION); Cursor c = h.iterator(); c.add(HeaderCard.create(Standard.SIMPLE, true)); c.add(HeaderCard.create(Standard.BITPIX, Bitpix.forPrimitiveType(getElementType()).getHeaderValue())); int[] dims = getDataDims(); c.add(HeaderCard.create(Standard.NAXIS, dims.length + 1)); h.addValue(Standard.NAXIS1, 0); for (int i = 1; i <= dims.length; i++) { c.add(HeaderCard.create(Standard.NAXISn.n(i + 1), dims[dims.length - i])); } // Just in case! c.add(HeaderCard.create(Standard.GROUPS, true)); c.add(HeaderCard.create(Standard.PCOUNT, getParameterCount())); c.add(HeaderCard.create(Standard.GCOUNT, groups)); c.add(HeaderCard.create(Standard.EXTEND, true)); Standard.context(null); } @Override protected long getTrueSize() { if (sampleRow == null) { return 0; } return (FitsEncoder.computeSize(sampleRow[0]) + FitsEncoder.computeSize(sampleRow[1])) * groups; } @Override public boolean isEmpty() { return dataArray.length == 0; } @Override protected void loadData(ArrayDataInput in) throws IOException { dataArray = new Object[groups][2]; for (int i = 0; i < groups; i++) { dataArray[i][0] = ((Object[]) ArrayFuncs.deepClone(sampleRow))[0]; dataArray[i][1] = ((Object[]) ArrayFuncs.deepClone(sampleRow))[1]; } in.readImage(dataArray); } @Override protected Object[][] getCurrentData() { return dataArray; } @Override public Object[][] getData() throws FitsException { return (Object[][]) super.getData(); } @SuppressWarnings({"resource", "deprecation"}) @Override public void write(ArrayDataOutput str) throws FitsException { if (getTrueSize() <= 0) { return; } if (str != getRandomAccessInput()) { ensureData(); } try { str.writeArray(dataArray); FitsUtil.pad(str, getTrueSize()); } catch (IOException e) { throw new FitsException("IO error writing random groups data ", e); } } @SuppressWarnings("deprecation") @Override public RandomGroupsHDU toHDU() throws FitsException { Header h = new Header(); fillHeader(h); return new RandomGroupsHDU(h, this); } /** * Returns the image component stored in the specified group. * * @param group The zero-based group index * * @return The image array for the specified group * * @throws ArrayIndexOutOfBoundsException if the group index is out of bounds * @throws FitsException if the deferred data could not be loaded. * * @see RandomGroupsHDU#getParameter(String, int) * * @since 1.19 */ public Object getImage(int group) throws ArrayIndexOutOfBoundsException, FitsException { ensureData(); return dataArray[group][1]; } Object getParameterArray(int group) throws ArrayIndexOutOfBoundsException, FitsException { ensureData(); return dataArray[group][0]; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/RandomGroupsHDU.java000066400000000000000000000464241476377620500247400ustar00rootroot00000000000000package nom.tam.fits; import java.io.PrintStream; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Hashtable; import java.util.Set; import nom.tam.fits.header.Bitpix; import nom.tam.fits.header.Standard; import nom.tam.util.ArrayDataOutput; import nom.tam.util.ArrayFuncs; import nom.tam.util.FitsOutput; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.fits.header.Standard.BITPIX; import static nom.tam.fits.header.Standard.GCOUNT; import static nom.tam.fits.header.Standard.GROUPS; import static nom.tam.fits.header.Standard.NAXIS; import static nom.tam.fits.header.Standard.NAXISn; import static nom.tam.fits.header.Standard.PCOUNT; import static nom.tam.fits.header.Standard.SIMPLE; import static nom.tam.fits.header.Standard.XTENSION; import static nom.tam.fits.header.Standard.XTENSION_IMAGE; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Random groups header/data unit. Random groups were an early attempt at extending FITS support beyond images, and was * eventually superseded by binary tables, which offer the same functionality and more in a more generic way. The use of * random group HDUs is discouraged, even by the FITS standard. Some old radio data may be packaged in this format. Thus * apart from provided limited support for reading such data, users should not create random groups anew. * {@link BinaryTableHDU} offers a much more flexible and capable way for storing an ensemble of parameters, arrays, and * more. *

* Note that the internal storage of random groups is a Object[ngroups][2] array. The first element of each * group (row) is a 1D array of parameter data of a numerical primitive type (e.g. short[], * double[]). The second element in each group (row) is an image of the same element type as the * parameters. When analyzing group data structure only the first group is examined, but for a valid FITS file all * groups must have the same structure. *

* As of version 1.19, we provide support for accessing parameters by names including building up higher-precision * values by combining multiple related parameter conversion recipes through scalings and offsets, as described in the * FITS standard (e.g. combining 3 or 4 related byte parameter values to obtain a full-precision 32-bit * float parameter value when BITPIX is 8). *

* * @see BinaryTableHDU */ @SuppressWarnings("deprecation") public class RandomGroupsHDU extends BasicHDU { private Hashtable parameters; @Override protected final String getCanonicalXtension() { return Standard.XTENSION_IMAGE; } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return a random groups data structure from an array of objects representing the data. * * @param o the array of object to create the random groups * * @throws FitsException if the data could not be created. */ @Deprecated public static RandomGroupsData encapsulate(Object o) throws FitsException { if (o instanceof Object[][]) { return new RandomGroupsData((Object[][]) o); } throw new FitsException("Attempt to encapsulate invalid data in Random Group"); } static Object[] generateSampleRow(Header h) throws FitsException { int ndim = h.getIntValue(NAXIS, 0) - 1; int[] dims = new int[ndim]; Class baseClass = Bitpix.fromHeader(h).getNumberType(); // Note that we have to invert the order of the axes // for the FITS file to get the order in the array we // are generating. Also recall that NAXIS1=0, so that // we have an 'extra' dimension. for (int i = 0; i < ndim; i++) { long cdim = h.getIntValue(NAXISn.n(i + 2), 0); if (cdim < 0) { throw new FitsException("Invalid array dimension:" + cdim); } dims[ndim - i - 1] = (int) cdim; } Object[] sample = new Object[2]; sample[0] = ArrayFuncs.newInstance(baseClass, h.getIntValue(PCOUNT)); sample[1] = ArrayFuncs.newInstance(baseClass, dims); return sample; } /** * Check if this data is compatible with Random Groups structure. Must be an Object[nGroups][2] * structure with both elements of each group having the same base type and the first element being a simple * primitive array. We do not check anything but the first row. * * @deprecated (for internal use) Will reduce visibility in the future * * @param potentialData data to check * * @return is this data compatible with Random Groups structure */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isData(Object potentialData) { if (potentialData instanceof Object[][]) { Object[][] o = (Object[][]) potentialData; if (o.length > 0 && o[0].length == 2 && // ArrayFuncs.getBaseClass(o[0][0]) == ArrayFuncs.getBaseClass(o[0][1])) { String cn = o[0][0].getClass().getName(); if (cn.length() == 2 && cn.charAt(1) != 'Z' || cn.charAt(1) != 'C') { return true; } } } return false; } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return Is this a random groups header? * * @param hdr The header to be tested. */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isHeader(Header hdr) { if (hdr.getBooleanValue(SIMPLE)) { return hdr.getBooleanValue(GROUPS); } String xtension = hdr.getStringValue(XTENSION); xtension = xtension == null ? "" : xtension.trim(); if (XTENSION_IMAGE.equals(xtension)) { return hdr.getBooleanValue(GROUPS); } return false; } /** * Prepares a data object into which the actual data can be read from an input subsequently or at a later time. * * @deprecated (for internal use) Will reduce visibility in the future * * @param header The FITS header that describes the data * * @return A data object that support reading content from a stream. * * @throws FitsException if the data could not be prepared to prescriotion. */ @Deprecated public static RandomGroupsData manufactureData(Header header) throws FitsException { int gcount = header.getIntValue(GCOUNT, -1); int pcount = header.getIntValue(PCOUNT, -1); if (!header.getBooleanValue(GROUPS) || header.getIntValue(NAXISn.n(1), -1) != 0 || gcount < 0 || pcount < 0 || header.getIntValue(NAXIS) < 2) { throw new FitsException("Invalid Random Groups Parameters"); } return new RandomGroupsData(gcount, generateSampleRow(header)); } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return Make a header point to the given object. * * @param d The random groups data the header should describe. * * @throws FitsException if the operation failed */ @Deprecated static Header manufactureHeader(Data d) throws FitsException { if (d == null) { throw new FitsException("Attempt to create null Random Groups data"); } Header h = new Header(); d.fillHeader(h); return h; } /** * Creates a random groups HDU from an Object[nGroups][2] array. Prior to 1.18, we used * {@link Fits#makeHDU(Object)} to create random groups HDUs automatically from matching data. However, FITS * recommends using binary tables instead of random groups in general, and this type of HDU is included in the * standard only to support reading some older radio data. Hence, as of 1.18 {@link Fits#makeHDU(Object)} will never * return random groups HDUs any longer, and will instead create binary (or ASCII) table HDUs instead. If the need * arises to create new random group HDUs programatically, beyond reading of older files, then this method can take * its place. * * @param data The random groups table. The second dimension must be 2. The first element in each group * (row) must be a 1D numerical primitive array, while the second element may be a * multi-dimensional image of the same element type. All rows must consists of arrays of * the same primitive numerical types and sized, e.g. * { float[5], float[7][2] } or { short[3], short[2][2][4] }. * * @return a new random groups HDU, which encapsulated the supploed data table. * * @throws FitsException if the seconds dimension of the array is not 2. * * @see Fits#makeHDU(Object) * * @since 1.18 */ public static RandomGroupsHDU createFrom(Object[][] data) throws FitsException { if (!isData(data)) { throw new FitsException("Type or layout of data is not random groups compatible."); } RandomGroupsData d = encapsulate(data); return new RandomGroupsHDU(manufactureHeader(d), d); } private void parseParameters(Header header) { // Parse the parameter descriptions from the header int nparms = header.getIntValue(Standard.PCOUNT); parameters = new Hashtable<>(); for (int i = 1; i <= nparms; i++) { String name = header.getStringValue(Standard.PTYPEn.n(i)); if (name == null) { continue; } Parameter p = parameters.get(name); if (p == null) { p = new Parameter(); parameters.put(name, p); } p.components.add(new ParameterConversion(header, i)); } } /** * Create an HDU from the given header and data. * * @deprecated (for internal use) Its visibility should be reduced to package level in the future. * * @param header header to use * @param data data to use */ public RandomGroupsHDU(Header header, RandomGroupsData data) { super(header, data); if (header == null) { return; } parseParameters(header); } @Override public void info(PrintStream stream) { stream.println("Random Groups HDU"); if (myHeader != null) { stream.println(" HeaderInformation:"); stream.println(" Ngroups:" + myHeader.getIntValue(GCOUNT)); stream.println(" Npar: " + myHeader.getIntValue(PCOUNT)); stream.println(" BITPIX: " + myHeader.getIntValue(BITPIX)); stream.println(" NAXIS: " + myHeader.getIntValue(NAXIS)); for (int i = 0; i < myHeader.getIntValue(NAXIS); i++) { stream.println(" NAXIS" + (i + 1) + "= " + myHeader.getIntValue(NAXISn.n(i + 1))); } } else { stream.println(" No Header Information"); } Object[][] data = null; if (myData != null) { try { data = myData.getData(); } catch (FitsException e) { // nothing to do... } } if (data == null || data.length < 1 || data[0].length != 2) { stream.println(" Invalid/unreadable data"); } else { stream.println(" Number of groups:" + data.length); stream.println(" Parameters: " + ArrayFuncs.arrayDescription(data[0][0])); stream.println(" Data:" + ArrayFuncs.arrayDescription(data[0][1])); } } /** * Returns the number of parameter bytes (per data group) accompanying each data object in the group. */ @Override public int getParameterCount() { return super.getParameterCount(); } /** * Returns the number of data objects (of identical shape and size) that are group together in this HDUs data * segment. */ @Override public int getGroupCount() { return super.getGroupCount(); } /** * Check that this HDU has a valid header. * * @return true if this HDU has a valid header. */ public boolean isHeader() { return isHeader(myHeader); } /** * Returns the name of the physical unit in which image data are represented. * * @return the standard name of the physical unit in which the image is expressed, e.g. "Jy beam^{-1}". */ @Override public String getBUnit() { return super.getBUnit(); } /** * Returns the integer value that signifies blank (missing or null) data in an integer image. * * @return the integer value used for identifying blank / missing data in integer images. * * @throws FitsException if the header does not specify a blanking value or if it is not appropriate for the type of * imge (that is not an integer type image) */ @Override public long getBlankValue() throws FitsException { if (getBitpix().getHeaderValue() < 0) { throw new FitsException("No integer blanking value in floating-point images."); } return super.getBlankValue(); } /** * Returns the floating-point increment between adjacent integer values in the image. Strictly speaking, only * integer-type images should define a quantization scaling, but there is no harm in having this value in * floating-point images also -- which may be interpreted as a hint for quantization, perhaps. * * @return the floating-point quantum that corresponds to the increment of 1 in the integer data representation. * * @see #getBZero() */ @Override public double getBScale() { return super.getBScale(); } /** * Returns the floating-point value that corresponds to an 0 integer value in the image. Strictly speaking, only * integer-type images should define a quantization scaling, but there is no harm in having this value in * floating-point images also -- which may be interpreted as a hint for quantization, perhaps. * * @return the floating point value that correspond to the integer 0 in the image data. * * @see #getBScale() */ @Override public double getBZero() { return super.getBZero(); } @Override public void write(ArrayDataOutput stream) throws FitsException { if (stream instanceof FitsOutput) { if (!((FitsOutput) stream).isAtStart()) { throw new FitsException("Random groups are only permitted in the primary HDU"); } } super.write(stream); } /** * Returns a list of parameter names bundled along the images in each group, as extracted from the PTYPE_n_ header * entries. * * @return A set containing the parameter names contained in this HDU * * @see #getParameter(String, int) * * @since 1.19 */ public Set getParameterNames() { return parameters.keySet(); } /** * Returns the value for a given group parameter. * * @param name the parameter name * @param group the zero-based group index * * @return the stored parameter value in the specified group, or {@link Double#NaN} * if the there is no such group. * * @throws ArrayIndexOutOfBoundsException if the group index is out of bounds. * @throws FitsException if the deferred parameter data cannot be accessed * * @see #getParameterNames() * @see RandomGroupsData#getImage(int) * * @since 1.19 */ public double getParameter(String name, int group) throws ArrayIndexOutOfBoundsException, FitsException { Parameter p = parameters.get(name); if (p == null) { return Double.NaN; } return p.getValue(getData().getParameterArray(group)); } /** * A conversion recipe from the native BITPIX type to a floating-point value. Each parameter may have multiple such * recipes, the sum of which can provide the required precision for the parameter regardless the BITPIX storage * type. * * @author Attila Kovacs * * @since 1.19 */ private static final class ParameterConversion { private int index; private double scaling; private double offset; private ParameterConversion(Header h, int n) { index = n - 1; scaling = h.getDoubleValue(Standard.PSCALn.n(n), 1.0); offset = h.getDoubleValue(Standard.PZEROn.n(n), 0.0); } } /** * Represents a single parameter in the random groups data. * * @author Attila Kovacs * * @since 1.19 */ private static final class Parameter { private ArrayList components = new ArrayList<>(); private double getValue(Object array) { double value = 0.0; for (ParameterConversion c : components) { double x = Array.getDouble(array, c.index); value += c.scaling * x + c.offset; } return value; } } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/TableData.java000066400000000000000000000354131476377620500235740ustar00rootroot00000000000000package nom.tam.fits; import nom.tam.util.ComplexValue; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** *

* Interface for accessing binary and ASCII table data. *

*

* Note, that this interface is the big sister {@link nom.tam.util.DataTable}, with nearly identical method signstures. * But there are differences too, such as the type of argument of setting row data, and when and what excpetions are * thrown. This one also has includes additional methods for table manipulation. Overall it would have been a more * prudent design to consolidate the two interfaces but this is what we have so we stick to it. However, mabe this is * something an upcoming major release may address... *

* * @see nom.tam.util.DataTable */ public interface TableData { /** * Add a column to the table, without updating the header of an encompassing HDU. You should not use this method on * tables already in an HDU, since it will not update the HDUs headers. Instead you can either use * {@link TableHDU#addColumn(Object)} or else create a new HDU for the table once the editing is copmleted -- adding * or migrating any custom header entries as necessary after. * * @param newCol the new column information. it should be either a primitive array, in which each element * stores a scalar value for every row, or else an Object[] where type of all * of the constituents is identical. Multidimensional data should have the same layout in * each row, but varied length one-dimensional arrays are OK. The arrat's length must * match the number of rows already contained in the table, unless the table is still * empty. * * @return the number of columns in the adapted table * * @see TableHDU#addColumn(Object) * @see #deleteColumns(int, int) * @see #addRow(Object[]) * * @throws FitsException if the operation failed */ int addColumn(Object newCol) throws FitsException; /** * Add a row at the end of the table without updating the header of an encompassing HDU. You should not use this * method on tables already in an HDU, since it will not update the HDUs headers. Instead you can use * {@link TableHDU#addRow(Object[])} or else create a new HDU for the table once the editing is completed -- adding * or migrating any custom header entries as necessary after. * * @param newRow An array of elements to be added. Each element of o should be an array of primitives or a * String. * * @throws FitsException if the operation failed * * @return the number of rows in the adapted table * * @see TableHDU#addColumn(Object) * @see #setRowEntries(int, Object...) * @see #deleteRows(int, int) * @see #addColumn(Object) */ int addRow(Object[] newRow) throws FitsException; /** * Like {@link #addRow(Object[])}, but with a vararg list of row entries. * * @param entries A vararg list of elements to be added. Each element of o should be an array of primitives * or a String. * * @throws FitsException if the operation failed * * @return the number of rows in the adapted table * * @see #setRow(int, Object[]) * @see #addRowEntries(Object...) * @see #deleteRows(int, int) */ default int addRowEntries(Object... entries) throws FitsException { return addRow(entries); } /** * Removes a set of consecutive columns from this table, without updating assocu=iated the header information for * the columns that were removed. You should not use this method on tables already in an HDU, since it will not * update the HDUs headers. Instead you should always create a new HDU for the table after editing, adding or * migrating any custom header entries as necessary after. * * @param col the 0-based index of the first column to remove * @param len the number of subsequent columns to remove * * @throws FitsException if the table could not be modified * * @see #addColumn(Object) * @see #deleteRows(int, int) * @see #updateAfterDelete(int, Header) */ void deleteColumns(int col, int len) throws FitsException; /** * Removes a set of consecutive rows from this table without updating any associated header information for an * encompassing HDU. You should not use this method on tables already in an HDU, since it will not update the HDUs * headers. Instead you should always create a new HDU for the table after editing, adding or migrating any custom * header entries as necessary after. * * @param row the 0-based index of the first row to remove * @param len the number of subsequent rows to remove * * @throws FitsException if the table could not be modified * * @see #addRow(Object[]) * @see #deleteColumns(int, int) */ void deleteRows(int row, int len) throws FitsException; /** *

* Returns the data for a particular column in as an array of elements. See {@link #addColumn(Object)} for more * information about the format of data elements in general. *

* * @param col The 0-based column index. * * @return an array of primitives (for scalar columns), or else an Object[] array, or * possibly null * * @throws FitsException if the table could not be accessed * * @see #setColumn(int, Object) * @see #getElement(int, int) * @see #getNCols() */ Object getColumn(int col) throws FitsException; /** *

* Returns the data element in this table. Elements are always stored as arrays even when scalar types. Thus a * single double value will be returned as a double[1]. For most column types the storage * type of the array matches that of their native Java type, but there are exceptions: *

*
    *
  • Character arrays in FITS are stored as byte[] or short[], depending on the * {@link nom.tam.fits.FitsFactory#setUseUnicodeChars(boolean)} setting, not unicode Java char[]. * Therefore, this call will return byte[] or short[], the same as for a byte or 16-bit * integer array. As a result if a new table is created with the returned data, the new table column will change its * FITS column type from A to B or I.
  • *
  • Complex values in FITS are stored as float[2] or double[2], not as a * {@link ComplexValue} type. Therefore, this call will return float[] or double[], the * same as for a float array. As a result if a new table is created with the returned data, the new table column * will change it's FITS column type from C to F, or from M to * D,.
  • *
* * @param row the 0-based row index of the element * @param col the 0-based column index of the element * * @return A primitive array containing the data for the the specified (row, col) entry in the table. * * @throws FitsException if the table could not be accessed * * @see #setElement(int, int, Object) * @see #getNRows() * @see #getNCols() */ Object getElement(int row, int col) throws FitsException; /** * Returns the number of columns contained in this table. * * @return the current number of columns in the table. * * @see #getNRows() * @see #getColumn(int) * @see #setColumn(int, Object) */ int getNCols(); /** * Returns the number of columns contained in this table. * * @return the current number of columns in the table. * * @see #getNRows() * @see #getColumn(int) * @see #setColumn(int, Object) */ int getNRows(); /** * Returns an array of elements in a particualr table row. See {@link #getElement(int, int)} for more information * about the format of each element in the row. * * @param row the 0-based row index * * @return an object containing the row data (for all column) of the specified row, or possubly * null. See {@link #getElement(int, int)} for more information about the * format of each element in the row. * * @throws FitsException if the table could not be accessed * * @see #getNRows() * @see #setRow(int, Object[]) * @see #getColumn(int) * @see #getElement(int, int) */ Object[] getRow(int row) throws FitsException; /** * Sets new data for a table column. See {@link #addColumn(Object)} for more information on the column data format. * * @param col the 0-based column index * @param newCol an object containing the new column data (for all rows) of the specified column. See * {@link #getColumn(int)} for more information on the column data format. * * @throws FitsException if the table could not be modified * * @see #getNCols() * @see #getColumn(int) * @see #setRow(int, Object[]) * @see #setElement(int, int, Object) */ void setColumn(int col, Object newCol) throws FitsException; /** * Sets new data element in this table. See {@link #getElement(int, int)} for more information about the format of * elements. * * @param row the 0-based row index of the element * @param col the 0-based column index of the element * @param element the new element at the specified table location as a primitive array. * * @throws FitsException if the table could not be modified * * @see #getElement(int, int) * @see #getNRows() * @see #getNCols() */ void setElement(int row, int col, Object element) throws FitsException; /** * Sets new data for a table row. See {@link #getElement(int, int)} for more information about the format of * elements. * * @param row the 0-based row index * @param newRow an object containing the row data (for all column) of the specified row. See * {@link #getElement(int, int)} for more information about the format of each element in * the row. * * @throws FitsException if the table could not be modified * * @see #setRowEntries(int, Object...) * @see #getNRows() * @see #getRow(int) * @see #setColumn(int, Object) * @see #setElement(int, int, Object) */ void setRow(int row, Object[] newRow) throws FitsException; /** * Like {@link #setRow(int, Object[])} but with vararg list of entries. * * @param row the 0-based row index * @param entries an object containing the row data (for all column) of the specified row. See * {@link #getElement(int, int)} for more information about the format of each element in * the row. * * @throws FitsException if the table could not be modified * * @see #setRow(int, Object[]) * @see #addRowEntries(Object...) * @see #setElement(int, int, Object) */ default void setRowEntries(int row, Object... entries) throws FitsException { setRow(row, entries); } /** * Updates the table dimensions in the header following deletion. Whoever calls {@link #deleteColumns(int, int)} on * this table should call this method after the deletion(s), at least once after all desired column deletions have * been processed). * * @param oldNcol The number of columns in the table before the first call to * {@link #deleteColumns(int, int)}. * @param hdr The table header * * @throws FitsException if the header could not be updated * * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to (re)wrap * tables in a new HDU after column deletions, and then edit the new header as * necessary to incorporate custom entries. May be removed from the API in the future. */ void updateAfterDelete(int oldNcol, Header hdr) throws FitsException; } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/TableHDU.java000066400000000000000000000742331476377620500233460ustar00rootroot00000000000000package nom.tam.fits; import nom.tam.fits.header.GenericKey; import nom.tam.fits.header.IFitsHeader; import static nom.tam.fits.header.Standard.NAXISn; import static nom.tam.fits.header.Standard.TFIELDS; import static nom.tam.fits.header.Standard.TFORMn; import static nom.tam.fits.header.Standard.TTYPEn; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * Base class for binary and ASCII table implementations. * * @param the generic type of table data contained in this HDU instance. */ @SuppressWarnings("deprecation") public abstract class TableHDU extends BasicHDU { /** * Returns the default name for a columns with the specified index, to use if no column name was explicitly defined * * @param col The zero-based Java index of the column * * @return The default column name to use if no other name was defined. * * @since 1.20 * * @author Attila Kovacs * * @see #setColumnName(int, String, String) */ public static String getDefaultColumnName(int col) { return "Column " + (col + 1); } /** * Create the TableHDU. Note that this will normally only be invoked by subclasses in the FITS package. * * @deprecated intended for internal use. Its visibility should be reduced to package level in the future. * * @param hdr the header * @param td The data for the table. */ protected TableHDU(Header hdr, T td) { super(hdr, td); } /** * Add a column to the table without any associated header information. * * @param newCol the new column information. the newCol should be an Object[] where type of all of the * constituents is identical. The length of data should match the other columns. * Note: It is valid for data to be a 2 or higher dimensionality primitive array. In * this case the column index is the first (in Java speak) index of the array. E.g., if * called with int[30][20][10], the number of rows in the table should be 30 and this * column will have elements which are 2-d integer arrays with TDIM = (10,20). * * @return the number of columns in the adapted table * * @throws FitsException if the operation failed */ public int addColumn(Object newCol) throws FitsException { int nCols = getNCols(); myHeader.findCard(TFIELDS).setValue(nCols); return nCols; } /** * Add a row to the end of the table. If this is the first row, then this will add appropriate columns for each of * the entries. The rows to add must be supplied as column based array of arrays. * * @return the number of rows in the adapted table * * @param newRows rows to add to the table * * @throws FitsException if the operation failed */ public int addRow(Object[] newRows) throws FitsException { int row = myData.addRow(newRows); myHeader.findCard(NAXISn.n(2)).setValue(getNRows()); return row; } /** * Returns the list of column description keyword stems that descrive this column in the FITS header. * * @return the stems of the keywords that are associated with table columns. Users can supplement this with their * own and call the appropriate deleteColumns fields. */ protected abstract IFitsHeader[] columnKeyStems(); /** * Delete a set of columns from a table. * * @param column The one-indexed start column. * @param len The number of columns to delete. * * @throws FitsException if the operation failed * * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to use * {@link TableData#deleteColumns(int, int)} to edit tables before wrapping them in an * HDU and editing the header as necessary to incorporate custom entries. May be * removed from the API in the future. */ public void deleteColumnsIndexOne(int column, int len) throws FitsException { deleteColumnsIndexZero(column - 1, len); } /** * Delete a set of columns from a table. * * @param column The one-indexed start column. * @param len The number of columns to delete. * @param fields Stems for the header fields to be removed for the table. * * @throws FitsException if the operation failed * * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to use * {@link TableData#deleteColumns(int, int)} to edit tables before wrapping them in an * HDU and editing the header as necessary to incorporate custom entries. May be * removed from the API in the future. */ public void deleteColumnsIndexOne(int column, int len, String[] fields) throws FitsException { deleteColumnsIndexZero(column - 1, len, GenericKey.create(fields)); } /** * Delete a set of columns from a table. * * @param column The one-indexed start column. * @param len The number of columns to delete. * * @throws FitsException if the operation failed * * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to use * {@link TableData#deleteColumns(int, int)} to edit tables before wrapping them in an * HDU and editing the header as necessary to incorporate custom entries. May be * removed from the API in the future. */ public void deleteColumnsIndexZero(int column, int len) throws FitsException { deleteColumnsIndexZero(column, len, columnKeyStems()); } /** * Delete a set of columns from a table. * * @param column The zero-indexed start column. * @param len The number of columns to delete. * @param fields Stems for the header fields to be removed for the table. * * @throws FitsException if the operation failed * * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to use * {@link TableData#deleteColumns(int, int)} to edit tables before wrapping them in an * HDU and editing the header as necessary to incorporate custom entries. May be * removed from the API in the future. */ public void deleteColumnsIndexZero(int column, int len, IFitsHeader[] fields) throws FitsException { if (column < 0 || len < 0 || column + len > getNCols()) { throw new FitsException("Illegal columns deletion request- Start:" + column + " Len:" + len + " from table with " + getNCols() + " columns"); } if (len == 0) { return; } int ncol = getNCols(); myData.deleteColumns(column, len); // Get rid of the keywords for the deleted columns for (int col = column; col < column + len; col++) { for (IFitsHeader field : fields) { myHeader.deleteKey(field.n(col + 1)); } } // Shift the keywords for the columns after the deleted columns for (int col = column + len; col < ncol; col++) { for (IFitsHeader field : fields) { IFitsHeader oldKey = field.n(col + 1); IFitsHeader newKey = field.n(col + 1 - len); if (myHeader.containsKey(oldKey)) { myHeader.replaceKey(oldKey, newKey); } } } // Update the number of fields. myHeader.getCard(TFIELDS).setValue(getNCols()); // Give the data sections a chance to update the header too. myData.updateAfterDelete(ncol, myHeader); } /** * Remove all rows from the table starting at some specific index from the table. Inspired by a routine by R. Mathar * but re-implemented using the DataTable and changes to AsciiTable so that it can be done easily for both Binary * and ASCII tables. * * @param row the (0-based) index of the first row to be deleted. * * @throws FitsException if an error occurs. * * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to use * {@link TableData#deleteRows(int, int)} to edit tables before wrapping them in an * HDU and editing the header as necessary to incorporate custom entries. May be * removed from the API in the future. */ public void deleteRows(final int row) throws FitsException { deleteRows(row, getNRows() - row); } /** * Remove a number of adjacent rows from the table. This routine was inspired by code by R.Mathar but re-implemented * using changes in the ColumnTable class abd AsciiTable so that we can do it for all FITS tables. * * @param firstRow the (0-based) index of the first row to be deleted. This is zero-based indexing: * 0<=firstrow< number of rows. * @param nRow the total number of rows to be deleted. * * @throws FitsException If an error occurs in the deletion. * * @deprecated It is not entirely foolproof for keeping the header in sync -- it is better to use * {@link TableData#deleteRows(int, int)} to edit tables before wrapping them in an * HDU and editing the header as necessary to incorporate custom entries. May be * removed from the API in the future. */ public void deleteRows(final int firstRow, int nRow) throws FitsException { // Just ignore invalid requests. if (nRow <= 0 || firstRow >= getNRows() || firstRow <= 0) { return; } /* correct if more rows are requested than available */ if (nRow > getNRows() - firstRow) { nRow = getNRows() - firstRow; } myData.deleteRows(firstRow, nRow); myHeader.setNaxis(2, getNRows()); } /** * Find the 0-based column index corresponding to a particular column name. * * @return index of the column * * @param colName the name of the column */ public int findColumn(String colName) { for (int i = 0; i < getNCols(); i++) { String val = myHeader.getStringValue(TTYPEn.n(i + 1)); if (val != null && val.trim().equals(colName)) { return i; } } return -1; } /** *

* Returns the data for a particular column in as an array of elements. See {@link TableData#addColumn(Object)} for * more information about the format of data elements in general. *

* * @param col The 0-based column index. * * @return an array of primitives (for scalar columns), or else an Object[] array, or * possibly null * * @throws FitsException if the table could not be accessed * * @see TableData#getColumn(int) * @see #setColumn(int, Object) * @see #getElement(int, int) * @see #getNCols() */ public Object getColumn(int col) throws FitsException { return myData.getColumn(col); } /** *

* Returns the data for a particular column in as an array of elements. See {@link TableData#addColumn(Object)} for * more information about the format of data elements in general. *

* * @param colName The name or ID of the column as stored by the TTYPEn FITS header * keyword. * * @return an array of primitives (for scalar columns), or else an Object[] array, or * possibly null * * @throws FitsException if the table could not be accessed * * @see TableData#getColumn(int) * @see #setColumn(int, Object) * @see #getElement(int, int) * @see #getNCols() */ public Object getColumn(String colName) throws FitsException { return getColumn(findColumn(colName)); } /** * Get the FITS type of a column in the table. * * @param index The 0-based index of the column. * * @return The FITS type. * * @throws FitsException if an invalid index was requested. */ public String getColumnFormat(int index) throws FitsException { int flds = myHeader.getIntValue(TFIELDS, 0); if (index < 0 || index >= flds) { throw new FitsException("Bad column index " + index + " (only " + flds + " columns)"); } return myHeader.getStringValue(TFORMn.n(index + 1)).trim(); } /** * Convenience method for getting column data. Note that this works only for metadata that returns a string value. * This is equivalent to getStringValue(type+index); * * @return meta data string value * * @param index index of the colum * @param type the key type to get */ public String getColumnMeta(int index, String type) { return myHeader.getStringValue(type + (index + 1)); } /** * Gets the name of a column in the table, as it appears in this HDU's header. It may differ from a more currently * assigned name of the binary table data column after the HDU creation or reading. * * @param index The 0-based column index. * * @return The column name. * * @see BinaryTable.ColumnDesc#name() */ public String getColumnName(int index) { String ttype = myHeader.getStringValue(TTYPEn.n(index + 1)); if (ttype != null) { ttype = ttype.trim(); } return ttype; } /** *

* Returns the data for all columns in as an array. See {@link TableData#addColumn(Object)} for more information * about the column format of each element in the returned array. *

* * @return An array containing the column data for all columns. Each entry in the returned array is * itself an array of primitives (for scalar columns), or else an Object[] * array, or possibly null. * * @throws FitsException if the table could not be accessed * * @see TableData#getColumn(int) * @see #setColumn(int, Object) * @see #getElement(int, int) * @see #getNCols() */ public Object[] getColumns() throws FitsException { Object[] result = new Object[getNCols()]; for (int i = 0; i < result.length; i++) { result[i] = getColumn(i); } return result; } /** * Returns a specific element from this table * * @return a specific element of the table using 0-based indices. * * @param row the row index of the element * @param col the column index of the element * * @throws FitsException if the operation failed * * @see #getElement(int, int) */ public Object getElement(int row, int col) throws FitsException { return myData.getElement(row, col); } /** * Get the number of columns for this table * * @return The number of columns in the table. */ public int getNCols() { return myData.getNCols(); } /** * Get the number of rows for this table * * @return The number of rows in the table. */ public int getNRows() { return myData.getNRows(); } /** * Returns a specific row from this table * * @return a specific row of the table. * * @param row the index of the row to retreive * * @throws FitsException if the operation failed * * @see #setRow(int, Object[]) */ public Object[] getRow(int row) throws FitsException { return myData.getRow(row); } /** * Update a column within a table. The new column should have the same format ast the column being replaced. See * {@link TableData#addColumn(Object)} for more information about the column data format. * * @param col index of the column to replace * @param newCol the replacement column * * @throws FitsException if the operation failed * * @see #getColumn(int) * @see #setColumn(String, Object) * @see TableData#addColumn(Object) */ public void setColumn(int col, Object newCol) throws FitsException { myData.setColumn(col, newCol); } /** * Update a column within a table. The new column should have the same format as the column being replaced. See * {@link TableData#addColumn(Object)} for more information about the column data format. * * @param colName name of the column to replace * @param newCol the replacement column * * @throws FitsException if the operation failed * * @see #getColumn(String) * @see #setColumn(int, Object) * @see TableData#addColumn(Object) */ public void setColumn(String colName, Object newCol) throws FitsException { setColumn(findColumn(colName), newCol); } /** * Specify column metadata for a given column in a way that allows all of the column metadata for a given column to * be organized together. * * @param index The 0-based index of the column * @param key The column key. I.e., the keyword will be key+(index+1) * @param value The value to be placed in the header. * @param comment The comment for the header * @param after Should the header card be after the current column metadata block * (true), or immediately before the TFORM card (false). * * @throws HeaderCardException if the header could not be updated */ public void setColumnMeta(int index, IFitsHeader key, String value, String comment, boolean after) throws HeaderCardException { setCurrentColumn(index, after); myHeader.addLine(new HeaderCard(key.n(index + 1).key(), value, comment)); } /** * Specify column metadata for a given column in a way that allows all of the column metadata for a given column to * be organized together. * * @param index The 0-based index of the column * @param key The column key. I.e., the keyword will be key+(index+1) * @param value The value to be placed in the header. * @param comment The comment for the header * @param after Should the header card be after the current column metadata block * (true), or immediately before the TFORM card (false). * * @throws HeaderCardException if the header could not be updated * * @since 1.16 */ public void setColumnMeta(int index, IFitsHeader key, Number value, String comment, boolean after) throws HeaderCardException { setCurrentColumn(index, after); myHeader.addLine(new HeaderCard(key.n(index + 1).key(), value, comment)); } /** * Specify column metadata for a given column in a way that allows all of the column metadata for a given column to * be organized together. * * @param index The 0-based index of the column * @param key The column key. I.e., the keyword will be key+(index+1) * @param value The value to be placed in the header. * @param comment The comment for the header * @param after Should the header card be after the current column metadata block * (true), or immediately before the TFORM card (false). * * @throws HeaderCardException if the header could not be updated */ public void setColumnMeta(int index, String key, Boolean value, String comment, boolean after) throws HeaderCardException { setCurrentColumn(index, after); myHeader.addLine(new HeaderCard(key + (index + 1), value, comment)); } /** * Specify column metadata for a given column in a way that allows all of the column metadata for a given column to * be organized together. * * @param index The 0-based index of the column * @param key The column key. I.e., the keyword will be key+(index+1) * @param value The value to be placed in the header. * @param comment The comment for the header * @param after Should the header card be after the current column metadata block * (true), or immediately before the TFORM card (false). * * @throws HeaderCardException if the header could not be updated */ public void setColumnMeta(int index, String key, Number value, String comment, boolean after) throws HeaderCardException { setCurrentColumn(index, after); myHeader.addLine(new HeaderCard(key + (index + 1), value, comment)); } /** * Specify column metadata for a given column in a way that allows all of the column metadata for a given column to * be organized together. * * @param index The 0-based index of the column * @param key The column key. I.e., the keyword will be key+(index+1) * @param value The value to be placed in the header. * @param precision The maximum number of decimal places to show after the leading figure. (Trailing * zeroes will be ommitted.) * @param comment The comment for the header * @param after Should the header card be after the current column metadata block * (true), or immediately before the TFORM card (false). * * @throws HeaderCardException if the header could not be updated */ public void setColumnMeta(int index, String key, Number value, int precision, String comment, boolean after) throws HeaderCardException { setCurrentColumn(index, after); myHeader.addLine(new HeaderCard(key + (index + 1), value, precision, comment)); } /** * Specify column metadata for a given column in a way that allows all of the column metadata for a given column to * be organized together. * * @param index The 0-based index of the column * @param key The column key. I.e., the keyword will be key+(index+1) * @param value The value to be placed in the header. * @param comment The comment for the header * * @throws HeaderCardException if the header could not be updated */ public void setColumnMeta(int index, String key, String value, String comment) throws HeaderCardException { setColumnMeta(index, key, value, comment, true); } /** * Specify column metadata for a given column in a way that allows all of the column metadata for a given column to * be organized together. * * @param index The 0-based index of the column * @param key The column key. I.e., the keyword will be key+(index+1) * @param value The value to be placed in the header. * @param comment The comment for the header * @param after Should the header card be after the current column metadata block (true), or * immediately before the TFORM card (false). @throws FitsException if the * operation failed * * @throws HeaderCardException if the header could not be updated * * @deprecated use {@link #setColumnMeta(int, IFitsHeader, String, String, boolean)} */ @Deprecated public void setColumnMeta(int index, String key, String value, String comment, boolean after) throws HeaderCardException { setCurrentColumn(index, after); myHeader.addLine(new HeaderCard(key + (index + 1), value, comment)); } /** * Sets the name / ID of a specific column in this table. Naming columns is generally a good idea so that people can * figure out what sort of data actually appears in specific table columns. * * @param index the column index * @param name the name or ID we want to assing to the column * @param comment Any additional comment we would like to store alongside in the FITS header. * (The comment may be truncated or even ommitted, depending on space * constraints in the FITS header. * * @throws IndexOutOfBoundsException if the table has no column matching the index * @throws HeaderCardException if there was a problem wil adding the associated descriptive FITS header * keywords to this table's header. * * @see #getColumnName(int) * @see #getDefaultColumnName(int) */ public void setColumnName(int index, String name, String comment) throws IndexOutOfBoundsException, HeaderCardException { if (index < 0 || index >= getNCols()) { throw new IndexOutOfBoundsException( "column index " + index + " is out of bounds for table with " + getNCols() + " columns"); } setColumnMeta(index, TTYPEn, name, comment, true); } /** * Set the cursor in the header to point after the metadata for the specified column * * @param col The 0-based index of the column * * @deprecated (for internal use) Will be removed int the future (no longer used). */ public void setCurrentColumn(int col) { setCurrentColumn(col, true); } /** * Set the cursor in the header to point either before the TFORMn value or after the column metadata * * @param col The 0-based index of the column * @param after True if the cursor should be placed after the existing column metadata or false if the cursor * is to be placed before the TFORM value. If no corresponding TFORM is found, the cursor will * be placed at the end of current header. * * @deprecated (for internal use) Will have private access in the future. */ public void setCurrentColumn(int col, boolean after) { if (after) { myHeader.positionAfterIndex(TFORMn, col + 1); } else { myHeader.findCard(TFORMn.n(col + 1)); } } /** * Update a single element within the table. * * @param row the row index * @param col the column index * @param element the replacement element * * @throws FitsException if the operation failed * * @see #getElement(int, int) */ public void setElement(int row, int col, Object element) throws FitsException { myData.setElement(row, col, element); } /** * Update a row within a table. * * @param row row index * @param newRow the replacement row * * @throws FitsException if the operation failed * * @see #getRow(int) */ public void setRow(int row, Object[] newRow) throws FitsException { myData.setRow(row, newRow); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/TruncatedFileException.java000066400000000000000000000045651476377620500263670ustar00rootroot00000000000000package nom.tam.fits; /* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * When an EOF is encountered in the middle of an HDU. * * @see FitsFactory#setAllowTerminalJunk(boolean) */ public class TruncatedFileException extends FitsException { /** * */ private static final long serialVersionUID = -4824457442211702123L; /** * Instantiates this exception with the designated message string. * * @param msg * a human readable message that describes what in fact caused * the exception */ public TruncatedFileException(String msg) { super(msg); } /** * Instantiates this exception with the designated message string, when it * was triggered by some other type of exception * * @param msg * a human readable message that describes what in fact caused * the exception * @param cause * the original exception (or other throwable) that triggered * this exception. */ public TruncatedFileException(String msg, Exception cause) { super(msg, cause); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/UnclosedQuoteException.java000066400000000000000000000043141476377620500264200ustar00rootroot00000000000000/* * #%L * nom.tam FITS library * %% * Copyright (C) 2004 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ package nom.tam.fits; /** * A header value with an unclosed single quote. Thrown when the library does * not have automatic header repairs enabled at present. * * @author Attila Kovacs * @see FitsFactory#setAllowHeaderRepairs(boolean) * @since 1.16 */ public class UnclosedQuoteException extends HeaderCardException { /** * */ private static final long serialVersionUID = 1292006588668191639L; private static String getMessage(String line) { return "Unclosed quotes in: [" + line.trim() + "]" + "\n\n --> Try FitsFactory.setAllowHeaderRepairs(true).\n"; } /** * Instantiates a new exception indicated an unclosed string quote in a * parsed header value. * * @param line * the 80-character header record fro which the exception * occurred. */ public UnclosedQuoteException(String line) { super(getMessage(line)); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/UndefinedData.java000066400000000000000000000177301476377620500244500ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam.fits * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import nom.tam.fits.header.Bitpix; import nom.tam.fits.header.Standard; import nom.tam.util.ArrayDataInput; import nom.tam.util.ArrayDataOutput; import nom.tam.util.ArrayFuncs; import nom.tam.util.Cursor; import nom.tam.util.FitsEncoder; /** * A container for unknown binary data types. We can still retrieve the data as a byte[] array, we just * don't know how to interpret it ourselves. This class makes sure we don't break when we encouter HDUs that we don't * (yet) support, such as HDU types defined by future FITS standards. * * @see UndefinedHDU */ public class UndefinedData extends Data { private static final String XTENSION_UNKNOWN = "UNKNOWN"; // private static final Logger LOG = getLogger(UndefinedData.class); private Bitpix bitpix = Bitpix.BYTE; private int[] dims; private int byteSize = 0; private byte[] data; private int pCount = 0; private int gCount = 1; private String extensionType = XTENSION_UNKNOWN; /** * Creates a new empty container for data of unknown type based on the provided FITS header information. * * @param h The FITS header corresponding to the data segment in the HDU * * @throws FitsException if there wan an error accessing or interpreting the provided header information. * * @deprecated (for internal use). Visibility will be reduced to the package level in the * future. */ public UndefinedData(Header h) throws FitsException { extensionType = h.getStringValue(Standard.XTENSION, XTENSION_UNKNOWN); int naxis = h.getIntValue(Standard.NAXIS); dims = new int[naxis]; int size = naxis > 0 ? 1 : 0; for (int i = 1; i <= naxis; i++) { dims[naxis - i] = h.getIntValue(Standard.NAXISn.n(i)); size *= dims[naxis - i]; } pCount = h.getIntValue(Standard.PCOUNT); size += pCount; gCount = h.getIntValue(Standard.GCOUNT); if (gCount > 1) { size *= h.getIntValue(Standard.GCOUNT); } bitpix = Bitpix.fromHeader(h); size *= bitpix.byteSize(); byteSize = size; } /** * @deprecated (for internal use). Users should always construct known data types. * Reduce visibility to the package level. * * @param x object to create the hdu from * * @throws IllegalArgumentException If the object is not an array or contains elements that do not have a known * binary size. */ public UndefinedData(Object x) throws IllegalArgumentException { byteSize = (int) FitsEncoder.computeSize(x); dims = ArrayFuncs.getDimensions(x); data = new byte[byteSize]; ArrayFuncs.copyInto(x, data); } @SuppressWarnings("deprecation") @Override protected void fillHeader(Header head) { // We'll assume it's a primary image, until we know better... // Just in case, we don't want an XTENSION key lingering around... head.deleteKey(Standard.SIMPLE); head.deleteKey(Standard.EXTEND); Standard.context(UndefinedData.class); Cursor c = head.iterator(); c.add(HeaderCard.create(Standard.XTENSION, extensionType)); c.add(HeaderCard.create(Standard.BITPIX, bitpix.getHeaderValue())); c.add(HeaderCard.create(Standard.NAXIS, dims.length)); for (int i = 1; i <= dims.length; i++) { c.add(HeaderCard.create(Standard.NAXISn.n(i), dims[dims.length - i])); } c.add(HeaderCard.create(Standard.PCOUNT, pCount)); c.add(HeaderCard.create(Standard.GCOUNT, gCount)); Standard.context(null); } @Override protected byte[] getCurrentData() { return data; } @Override protected long getTrueSize() { return byteSize; } /** * Returns the FITS extension type as stored by the XTENSION keyword in the FITS header. * * @return The value used for the XTENSION keyword in the FITS header * * @since 1.19 */ public final String getXtension() { return extensionType; } /** * Returns the FITS element type as a Bitpux value. * * @return The FITS Bitpix value for the type of primitive data element used by this data * * @since 1.19 */ public final Bitpix getBitpix() { return bitpix; } /** * Returns the size of the optional parameter space as stored by the PCOUNT keyword in the FITS header. * * @return The element count of the optional parameter space accompanying the main data, as stored by the PCOUNT * header value. * * @since 1.19 */ public final int getParameterCount() { return pCount; } /** * Returns the number of repeated (data + parameter) groups in this data object * * @return The number of repeated data + parameter blocks, as stored by the GCOUNT header value. * * @since 1.19 */ public final int getGroupCount() { return gCount; } /** * Returns the dimensionality of the data (if any), in Java array index order. That is, The value for NAXIS1 is the * last value in the returned array * * @return the regular dimensions of the data in Java index order (that is NAXIS1 is the last entry in the array), * or possibly null if no dimensions have been defined. */ public final int[] getDimensions() { return dims; } @Override public byte[] getData() throws FitsException { byte[] bytes = (byte[]) super.getData(); if (bytes != null) { return bytes; } data = new byte[byteSize]; return data; } @Override protected void loadData(ArrayDataInput in) throws IOException { data = new byte[byteSize]; in.readFully(data); } @SuppressWarnings({"resource", "deprecation"}) @Override public void write(ArrayDataOutput o) throws FitsException { if (o != getRandomAccessInput()) { ensureData(); } try { o.write(data); } catch (IOException e) { throw new FitsException("IO Error on unknown data write", e); } FitsUtil.pad(o, getTrueSize()); } @Override @SuppressWarnings("deprecation") public UndefinedHDU toHDU() { Header h = new Header(); fillHeader(h); return new UndefinedHDU(h, this); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/UndefinedHDU.java000066400000000000000000000134551476377620500242170ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam.fits * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.PrintStream; import nom.tam.fits.header.Standard; import static nom.tam.fits.header.Standard.XTENSION; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * A HDU that holds a type of data we don't recognise. We can still access that data in its raw binary form, and the * user can interpret the headers to make sense of particular but not (yet) supported FITS HDU types. * * @see UndefinedData */ public class UndefinedHDU extends BasicHDU { @Override protected String getCanonicalXtension() { return "UNKNOWN"; } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return Encapsulate an object as an UndefinedHDU. * * @param o the object to encapsulate * * @throws FitsException if the operation failed */ @Deprecated public static UndefinedData encapsulate(Object o) throws FitsException { return new UndefinedData(o); } /** * Checks if we can use the following object as in an Undefined FITS block. Only byte[] arrays can be * represented in undefined HDUs. * * @deprecated (for internal use) Will reduce visibility in the future * * @param o a data object * * @return true if the object is a raw byte[] array, otherwise false. * We cannot wrap arbitrary data objects since we do not have a generic recipe for converting * these into binary form. */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isData(Object o) { return o instanceof byte[]; } /** * Checks if the header is for a HDU we don't really know how to handle. We can still retrieve and store the binary * tata of the HDU as a raw byte[] image. * * @deprecated (for internal use) Will reduce visibility in the future * * @param hdr header to check. * * @return true if this HDU has a valid header. */ @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility") @Deprecated public static boolean isHeader(Header hdr) { if (ImageHDU.isHeader(hdr)) { return false; } if (BinaryTableHDU.isHeader(hdr)) { return false; } if (AsciiTableHDU.isHeader(hdr)) { return false; } return hdr.containsKey(Standard.XTENSION); } /** * Prepares a data object into which the actual data can be read from an input subsequently or at a later time. * * @deprecated (for internal use) Will reduce visibility in the future * * @param hdr The FITS header that describes the data * * @return A data object that support reading content from a stream. * * @throws FitsException if the data could not be prepared to prescriotion. */ @Deprecated public static UndefinedData manufactureData(Header hdr) throws FitsException { return new UndefinedData(hdr); } /** * @deprecated (for internal use) Will reduce visibility in the future * * @return Create a header that describes the given image data. * * @param d The image to be described. * * @throws FitsException if the object does not contain valid image data. */ @Deprecated public static Header manufactureHeader(Data d) throws FitsException { Header h = new Header(); d.fillHeader(h); return h; } /** * Build an image HDU using the supplied data. * * @deprecated (for internal use) Its visibility should be reduced to package level in the future. * * @param h the header for this HDU * @param d the data used to build the image. */ public UndefinedHDU(Header h, UndefinedData d) { super(h, d); } @Override public void info(PrintStream stream) { stream.println(" Unhandled/Undefined/Unknown Type"); stream.println(" XTENSION=" + myHeader.getStringValue(XTENSION).trim()); stream.println(" Apparent size:" + myData.getTrueSize()); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/ValueTypeException.java000066400000000000000000000036271476377620500255520ustar00rootroot00000000000000package nom.tam.fits; /*- * #%L * nom.tam.fits * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * Invalid value type used for a standard header keyword. * * @author Attila Kovacs * @since 1.19 */ public class ValueTypeException extends HeaderCardException { private static final long serialVersionUID = -4338698718509151861L; /** * Creates a new exception * * @param key * the FITS keyword for which the exception occurred. * @param valueType * the type of value that was problematic. */ public ValueTypeException(String key, String valueType) { super(key + " does not support " + valueType + " type values"); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/000077500000000000000000000000001476377620500227355ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/BZip2CompressionProvider.java000066400000000000000000000046301476377620500304660ustar00rootroot00000000000000package nom.tam.fits.compress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.io.InputStream; /** * (for internal use) BZIP2 (.bz2) input stream decompression. You can use this class to decompress * files that have been compressed with bzip2, or use {@link CompressionManager} to automatically detect * the type of compression used. This class uses the Apache commons-compress package to perform the * decompression. * * @see CompressionManager */ public class BZip2CompressionProvider implements ICompressProvider { private static final int PRIORITY = 5; @SuppressWarnings("deprecation") @Override public InputStream decompress(InputStream in) throws IOException { try { return CompressionLibLoaderProtection.createBZip2Stream(in); } catch (IOException e) { throw e; } catch (Exception e) { throw new IOException(e); } } @Override public int priority() { return PRIORITY; } @Override public boolean provides(int mag1, int mag2) { return mag1 == 'B' && mag2 == 'Z'; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/BasicCompressProvider.java000066400000000000000000000100501476377620500300440ustar00rootroot00000000000000package nom.tam.fits.compress; import java.io.IOException; import java.io.InputStream; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.fits.FitsException; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.util.LoggerHelper.getLogger; /** * (for internal use) UNIX compressed (.Z) input stream decompression with a preference for using an * external system command. You can use this class to decompress files that have been compressed with the UNIX * compress tool (or via gzip) and have the characteristic .Z file name extension. It * effectively provides the same functionality as {@link ZCompressionProvider}, but has a preference for calling on the * system uncompress command first to do the lifting. If that fails it will call on {@link CompressionManager} to * provide a suitable decompressor (which will give it {@link ZCompressionProvider}). Since the compress tool is * UNIX-specific, it is not entirely portable. As a result, you are probably better off relying on those other classes * directly. * * @see CompressionManager * * @deprecated Use {@link ZCompressionProvider}. or the more generic {@link CompressionManager} with a preference toward * using the system command if possible. instead. */ @Deprecated public class BasicCompressProvider implements ICompressProvider { private static final int PRIORITY = 10; private static final int COMPRESS_MAGIC_BYTE1 = 0x1f; private static final int COMPRESS_MAGIC_BYTE2 = 0x9d; private static final Logger LOG = getLogger(BasicCompressProvider.class); private InputStream compressInputStream(final InputStream compressed) throws IOException, FitsException { try { Process proc = new ProcessBuilder("uncompress", "-c").start(); return new CloseIS(proc, compressed); } catch (Exception e) { ICompressProvider next = CompressionManager.nextCompressionProvider(COMPRESS_MAGIC_BYTE1, COMPRESS_MAGIC_BYTE2, this); if (next != null) { LOG.log(Level.WARNING, "Error initiating .Z decompression: " + e.getMessage() + " trying alternative decompressor", e); return next.decompress(compressed); } throw new FitsException("Unable to read .Z compressed stream.\nIs 'uncompress' in the path?", e); } } @Override public InputStream decompress(InputStream in) throws IOException, FitsException { return compressInputStream(in); } @Override public int priority() { return PRIORITY; } @Override public boolean provides(int mag1, int mag2) { return mag1 == COMPRESS_MAGIC_BYTE1 && mag2 == COMPRESS_MAGIC_BYTE2; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/CloseIS.java000066400000000000000000000152321476377620500251040ustar00rootroot00000000000000package nom.tam.fits.compress; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.logging.Level; import java.util.logging.Logger; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.util.LoggerHelper.getLogger; /** * (for internal use) Ensures that input streams aren't left open when decompressing with an external system * tool, such as the UNIX compress or bzip2 commands. It is discouraged to use system tools for * decompressing such files, especially since we have native Java implementations through Apache's * commons-compress classes. The system tools are not portable, whereas the commons-compress * implementation is. Therefore, you should neer really need to use this class, which is provided only for compatibility * with earlier versions of this library. * * @deprecated Needed only by deprecated compression classes. And it should not have visibility outside of this package * anyway. */ @Deprecated public class CloseIS extends FilterInputStream { private static final Logger LOG = getLogger(CloseIS.class); private static final int COPY_BUFFER_SIZE = 64 * 1024; private InputStream output; private OutputStream input; private String errorText; private IOException exception; private final Thread stdError; private final Thread copier; private final Process proc; /** * Instantiates a new thread that will watch and close the input stream whenever the process using it compeletes. * * @param proc The process that is using the input stream * @param compressed the compressed input stream that is used by the process. */ @SuppressWarnings("resource") public CloseIS(Process proc, final InputStream compressed) { super(new BufferedInputStream(proc.getInputStream(), CompressionManager.ONE_MEGABYTE)); if (compressed == null) { throw new NullPointerException(); } this.proc = proc; final InputStream error = proc.getErrorStream(); output = proc.getInputStream(); input = proc.getOutputStream(); stdError = new Thread(new Runnable() { @Override public void run() { try { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); byte[] buffer = new byte[COPY_BUFFER_SIZE]; int len; while ((len = error.read(buffer, 0, buffer.length)) >= 0) { bytes.write(buffer, 0, len); } error.close(); errorText = new String(bytes.toByteArray(), Charset.defaultCharset()); } catch (IOException e) { exception = e; } } }); // Now copy everything in a separate thread. copier = new Thread(new Runnable() { @Override public void run() { try { byte[] buffer = new byte[COPY_BUFFER_SIZE]; int len; while ((len = compressed.read(buffer, 0, buffer.length)) >= 0) { input.write(buffer, 0, len); } input.close(); } catch (IOException e) { exception = e; } try { compressed.close(); } catch (IOException e) { if (exception == null) { exception = e; } } } }); start(); } /** * start all threads. */ private void start() { stdError.start(); copier.start(); } @Override public int read() throws IOException { int result = 0; try { result = super.read(); return result; } catch (IOException e) { result = -1; throw e; } finally { handledOccuredException(result); } } private void handledOccuredException(int result) throws IOException { int exitValue = 0; if (result < 0) { try { stdError.join(); copier.join(); exitValue = proc.exitValue(); } catch (Exception e) { LOG.log(Level.WARNING, "could not join the stream processes", e); } } if (exception != null || exitValue != 0) { if (errorText != null && !errorText.trim().isEmpty()) { throw new IOException(errorText, exception); } if (exception == null) { throw new IOException("exit value was " + exitValue); } throw exception; } } @Override public int read(byte[] b, int off, int len) throws IOException { int result = 0; try { result = super.read(b, off, len); return result; } catch (IOException e) { throw e; } finally { handledOccuredException(result); } } @Override public void close() throws IOException { super.close(); input.close(); output.close(); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/CompressionLibLoaderProtection.java000066400000000000000000000057431476377620500317370ustar00rootroot00000000000000package nom.tam.fits.compress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.io.InputStream; /** * (for internal use) Shorthand access to Apache commons-compress * stream decompressors. Forgive the awkward name. * * @deprecated (for internal use) The visibility of this class may be * reduced to package level in the future. * @author Richard van Nieuwenhoven */ public final class CompressionLibLoaderProtection { private CompressionLibLoaderProtection() { } /** * Returns the Apache commons-compress decompressed input * stream for .bz2 compressed inputs * * @param in * the .bz2 compressed input stream * @return the decompressed input stream using Apache * commons-compress. * @throws IOException * if there was an IO error processing the input. */ public static InputStream createBZip2Stream(InputStream in) throws IOException { return new org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream(in); } /** * Returns the Apache commons-compress decompressed input * stream for .Z compressed inputs * * @param in * the .Z compressed input stream * @return the decompressed input stream using Apache * commons-compress. * @throws IOException * if there was an IO error processing the input. */ public static InputStream createZStream(InputStream in) throws IOException { return new org.apache.commons.compress.compressors.z.ZCompressorInputStream(in); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/CompressionManager.java000066400000000000000000000167351476377620500274100ustar00rootroot00000000000000package nom.tam.fits.compress; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.fits.FitsException; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.util.LoggerHelper.getLogger; /** * (for internal use) Decompression of compressed FITS files of all supported types (.gz, * .Z, .bz2). It autodetects the type of compression used based on the first 2-bytes of the * compressed input stream. When possible, preference will be given to perform the decompression using a system command * (uncompress or bzip2, which are likely faster for large files). If such a tool is not * available, then the Apache common-compress classes will be used to the same effect. * * @see GZipCompressionProvider * @see BZip2CompressionProvider * @see ZCompressionProvider */ public final class CompressionManager { private static final String BZIP2_EXTENTION = ".bz2"; private static final String COMPRESS_EXTENTION = ".Z"; private static final String GZIP_EXTENTION = ".gz"; /** bytes in a megabyte (MB) */ public static final int ONE_MEGABYTE = 1024 * 1024; /** * logger to log to. */ private static final Logger LOG = getLogger(CompressionManager.class); private CompressionManager() { } /** * This method decompresses a compressed input stream. The decompression method is selected automatically based upon * the first two bytes read. * * @param compressed The compressed input stream * * @return A stream which wraps the input stream and decompresses it. If the input stream is not * compressed, a pushback input stream wrapping the original stream is returned. * * @throws FitsException when the stream could not be read or decompressed */ public static InputStream decompress(InputStream compressed) throws FitsException { BufferedInputStream pb = new BufferedInputStream(compressed, ONE_MEGABYTE); pb.mark(2); int mag1 = -1; int mag2 = -1; try { mag1 = pb.read(); mag2 = pb.read(); // Push the data back into the stream pb.reset(); ICompressProvider selectedProvider = selectCompressionProvider(mag1, mag2); if (selectedProvider != null) { return selectedProvider.decompress(pb); } return pb; } catch (IOException e) { // This is probably a prelude to failure... throw new FitsException("Unable to analyze input stream", e); } } /** * Is a file compressed? (the magic number in the first 2 bytes is used to detect the compression. * * @param file file to test for compression algorithms * * @return true if the file is compressed */ public static boolean isCompressed(File file) { if (!file.exists()) { return false; } try (InputStream fis = new FileInputStream(file)) { int mag1 = fis.read(); int mag2 = fis.read(); fis.close(); return selectCompressionProvider(mag1, mag2) != null; } catch (IOException e) { LOG.log(Level.FINEST, "Error while checking if file " + file + " is compressed", e); } return false; } /** *

* Checks if a file is compressed. If the file by the name exists, it will check the magic number in the first * 2-bytes to see if they matched those of the supported compression algorithms. Otherwise it checks if the file * extension atches one of the standard extensions for supported compressed files (.gz, * .Z, or .bz2). *

*

* As of 1.18, all file extension are checked in a case insensitive manner *

* * @param filename of the file to test for compression algorithms * * @return true if the file is compressed */ public static boolean isCompressed(String filename) { if (filename == null) { return false; } File test = new File(filename); if (test.exists()) { return isCompressed(test); } int iExt = filename.lastIndexOf('.'); if (iExt < 0) { return false; } String ext = filename.substring(iExt); return ext.equalsIgnoreCase(GZIP_EXTENTION) || ext.equalsIgnoreCase(COMPRESS_EXTENTION) || ext.equalsIgnoreCase(BZIP2_EXTENTION); } private static ICompressProvider selectCompressionProvider(int mag1, int mag2) { return nextCompressionProvider(mag1, mag2, null); } /** * Returned the next highest priority decompression class, after the one we don't want, for the given type of * compressed file. * * @param mag1 the first magic byte at the head of the compressed file * @param mag2 the second magic byte at the head of the compressed file * @param old the last decompression class we tried for this type of file * * @return the next lower priority decompression class we might use. */ protected static ICompressProvider nextCompressionProvider(int mag1, int mag2, ICompressProvider old) { ICompressProvider selectedProvider = null; int priority = 0; int maxPriority = Integer.MAX_VALUE; if (old != null) { maxPriority = old.priority(); } ServiceLoader compressionProviders = ServiceLoader.load(ICompressProvider.class, Thread.currentThread().getContextClassLoader()); for (ICompressProvider provider : compressionProviders) { if (provider.priority() > Math.max(0, priority) && provider.priority() < maxPriority && provider != old && // provider.provides(mag1, mag2)) { priority = provider.priority(); selectedProvider = provider; } } return selectedProvider; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/ExternalBZip2CompressionProvider.java000066400000000000000000000114021476377620500321640ustar00rootroot00000000000000package nom.tam.fits.compress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.io.InputStream; import java.util.logging.Logger; import nom.tam.fits.FitsException; /** * (for internal use) BZIP2 (.bz2) input stream decompression with a preference for using an * external system command. You can use this class to decompress files that have been compressed with the UNIX * bzip2 tool and have the characteristic .bz2 file name extension. It effectively provides the same * functionality as {@link BZip2CompressionProvider}, but has a preference for calling on the system bzip2 * command first to do the lifting. If that fails it will call on {@link CompressionManager} to provide a suitable * decompressor (which will give it {@link BZip2CompressionProvider}). Since the bzip2 tool is UNIX-specific, it * is not entirely portable. It also requires the environment variable BZIP_DECOMPRESSOR to be set to * provide the system executable to use. As a result, you are probably better off relying on the mentioned other classes * directly for this functionality. * * @see CompressionManager * * @deprecated Use {@link ZCompressionProvider}, or the more generic {@link CompressionManager} with a preference toward * using the system command if possible, instead. */ @Deprecated public class ExternalBZip2CompressionProvider implements ICompressProvider { private static final int PRIORITY = 10; private static final Logger LOG = Logger.getLogger(ExternalBZip2CompressionProvider.class.getName()); private InputStream bunzipper(final InputStream compressed) throws IOException, FitsException { String cmd = getBzip2Cmd(); // Allow the user to have already specified the - option. if (cmd.indexOf(" -") < 0) { cmd += " -"; } String[] flds = cmd.split(" +"); Process proc; try { proc = new ProcessBuilder(flds).start(); return new CloseIS(proc, compressed); } catch (Exception e) { ICompressProvider next = CompressionManager.nextCompressionProvider('B', 'Z', this); if (next != null) { LOG.warning("Error initiating BZIP decompression: " + e.getMessage() + " trying alternative decompressor"); return next.decompress(compressed); } throw new FitsException("Error initiating BZIP decompression: " + e); } } /** * Returns the system command to use for decompressing .bz2 compressed files. It requires the * BZIP_DECOMPRESSOR environment variable to be set to inform us as to what executable (including path) * should be used. If there is no such environment variable set, it will return null * * @return The system command for decompressing .bz2 files, or null if there is no * BZIP_DECOMPRESSOR environment variable that could inform us. */ public String getBzip2Cmd() { return System.getProperty("BZIP_DECOMPRESSOR", System.getenv("BZIP_DECOMPRESSOR")); } @Override public InputStream decompress(InputStream in) throws IOException, FitsException { return bunzipper(in); } @Override public int priority() { return PRIORITY; } @Override public boolean provides(int mag1, int mag2) { return mag1 == 'B' && mag2 == 'Z' && getBzip2Cmd() != null; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/GZipCompressionProvider.java000066400000000000000000000045321476377620500304120ustar00rootroot00000000000000package nom.tam.fits.compress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.io.InputStream; import java.util.zip.GZIPInputStream; /** * (for internal use) GZIP (.gz) input stream decompression. You can use this class to decompress * files that have been compressed with gzip, or use {@link CompressionManager} to automatically detect the type * of compression used. This class uses Java's builtin {@link GZIPInputStream} class to handle the lifting. * * @see CompressionManager */ public class GZipCompressionProvider implements ICompressProvider { private static final int GZIP_MAGIC_BYTE1 = 0x1f; private static final int GZIP_MAGIC_BYTE2 = 0x8b; private static final int PRIORITY = 5; @Override public InputStream decompress(InputStream in) throws IOException { return new GZIPInputStream(in); } @Override public int priority() { return PRIORITY; } @Override public boolean provides(int mag1, int mag2) { return mag1 == GZIP_MAGIC_BYTE1 && mag2 == GZIP_MAGIC_BYTE2; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/ICompressProvider.java000066400000000000000000000062051476377620500272220ustar00rootroot00000000000000package nom.tam.fits.compress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.io.InputStream; import nom.tam.fits.FitsException; /** * (for internal use) Input stream decompression interface. */ public interface ICompressProvider { /** * Decompresses data from an input stream. * * @param in * the input stream containing compressed data * @return a new input stream containing the decompressed data * @throws IOException * if there was an IO error while accessing the input stream * @throws FitsException * if the decompression cannot be performed for some reason that * is not related to the input per se. */ InputStream decompress(InputStream in) throws IOException, FitsException; /** * Returns the priority of this method. {@link CompressionManager} will use * this to select the 'best' compression class when multiple compression * classes can provide decompression support for a given input stream. * Claases that have a higher priority will be preferred. * * @return the priority of this decompression method vs similar other * compression methods that may be avaialble. * @see CompressionManager */ int priority(); /** * Checks if this compression method can support the magic integer number * that is used to identify the type of compression at the beginning of * compressed files, and is stored as the first 2 bytes of compressed data. * * @param byte1 * the first byte of the compressed file * @param byte2 * the second byte of the compressed file * @return true if this class can be used to decompress the * given file, or else false. */ boolean provides(int byte1, int byte2); } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/ZCompressionProvider.java000066400000000000000000000052311476377620500277470ustar00rootroot00000000000000package nom.tam.fits.compress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.io.InputStream; /** * (for internal use) UNIX compressed (.Z) input stream decompression. You can use this class to * decompress files that have been compressed with the UNIX compress tool (or via gzip) and have the * characteristic .Z file name extension. Or, use {@link CompressionManager} to automatically detect the * type of compression used. This class uses the Apache commons-compress package to perform the decompression. * * @see CompressionManager */ public class ZCompressionProvider implements ICompressProvider { private static final int Z_COMPRESS_MAGIC_BYTE1 = 0x1f; private static final int Z_COMPRESS_MAGIC_BYTE2 = 0x9d; private static final int PRIORITY = 5; @SuppressWarnings("deprecation") @Override public InputStream decompress(InputStream in) throws IOException { try { return CompressionLibLoaderProtection.createZStream(in); } catch (IOException e) { throw e; } catch (Exception e) { throw new IOException(e); } } @Override public int priority() { return PRIORITY; } @Override public boolean provides(int mag1, int mag2) { return mag1 == Z_COMPRESS_MAGIC_BYTE1 && mag2 == Z_COMPRESS_MAGIC_BYTE2; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compress/package-info.java000066400000000000000000000031751476377620500261320ustar00rootroot00000000000000/** * (for internal use) Handling of compressed FITS files (file-level compression only). This is for file-level * compression support only. HDU-level compression, including tile compression is handled by the * {@link nom.tam.image.compression.hdu} package. * * @see nom.tam.image.compression.hdu */ package nom.tam.fits.compress; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/000077500000000000000000000000001476377620500234435ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/000077500000000000000000000000001476377620500254315ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/api/000077500000000000000000000000001476377620500262025ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/api/ICompressOption.java000066400000000000000000000115641476377620500321510ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.api; import nom.tam.fits.compression.provider.param.api.ICompressParameters; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * Option for the compression algorithm, implementors are used to control the compression algorithm. */ public interface ICompressOption extends Cloneable { /** * Returns an independent copy of this option. Modifications to the original or the copy will not affect the other. * * @return copy the option (normally the option from with the copy happened is saved as original). */ ICompressOption copy(); /** * (for internal use) Returns the parameters that represent the settings for this option in the FITS header * or compressed data column. * * @return the parameters that must be synchronized with the hdu meta data. * * @see #setParameters(ICompressParameters) */ ICompressParameters getCompressionParameters(); /** * Checks if this type of compression is inherently lossy * * @return true if the compression done with this specified options uses approximations. That means if * the reconstruction of the data is excact the return should be false. */ boolean isLossyCompression(); /** * (for internal use) Sets the parameters that link the options to how they are recorded in the FITS headers * or compressed table columns. * * @param parameters the parameters to synchronized * * @see #getCompressionParameters() */ void setParameters(ICompressParameters parameters); /** * Set the tile height (if the option supports it). If the implementing option class does not have a setting for * tile size, it should simply ignore the setting and return normally. * * @param value the new tile height in pixels * * @return itself * * @see #getTileHeight() * @see #setTileWidth(int) */ ICompressOption setTileHeight(int value); /** * Set the tile width (if the option supports it). If the implementing option class does not have a setting for tile * size, it should simply ignore the setting and return normally. * * @param value the new tile with in pixels * * @return itself * * @see #getTileWidth() * @see #setTileHeight(int) */ ICompressOption setTileWidth(int value); /** * Returns the tile height (if supported), or else 0 (also the default implementation). * * @return the tile height in pixels, or 0 if the options do not have a tile size setting. * * @see #setTileHeight(int) * @see #getTileWidth() * * @since 1.18 */ default int getTileHeight() { return 0; } /** * Returns the tile width (if supported), or else 0 (also the default implementation). * * @return the tile width in pixels, or 0 if the options do not have a tile size setting. * * @see #setTileHeight(int) * @see #getTileWidth() * * @since 1.18 */ default int getTileWidth() { return 0; } /** * (for internal use) Recasts these options for the specific implementation class * * @param clazz the implementation class * @param these options recast to the designated implementation type. * * @return the recast version of us or null if the recasting is not available for the specified * class type. */ T unwrap(Class clazz); } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/api/ICompressor.java000066400000000000000000000045301476377620500313140ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.api; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.nio.Buffer; import java.nio.ByteBuffer; /** * (for internal use) Compressor that can compress a Buffer into a ByteBuffer and vize versa. the Byte buffer * must have enough space allocated else an exception will be thrown. * * @param The generic type of NIO buffer on which this compressor operates. */ public interface ICompressor { /** * compress the buffer into the byte buffer. Attention enough space must already be allocated. * * @param buffer the buffer to compress. * @param compressed the compressed data * * @return true if the compression succeeded. */ boolean compress(T buffer, ByteBuffer compressed); /** * Decompress the byte buffer and restore the buffer from it, again enough space must already be allocated. * * @param compressed the compressed data * @param buffer the buffer to fill with the uncompressed data. */ void decompress(ByteBuffer compressed, T buffer); } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/api/ICompressorControl.java000066400000000000000000000051321476377620500326540ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.api; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.nio.Buffer; import java.nio.ByteBuffer; /** * (for internal use) The interface to a provided compression algorithm. */ public interface ICompressorControl { /** * Compress the buffer into the byte buffer using the specified options. * * @param in * the buffer to compress. * @param out * the compressed data to fill (must already be allocated with * enough space) * @param option * the options to use for the compression * @return true if the compression succeded. */ boolean compress(Buffer in, ByteBuffer out, ICompressOption option); /** * decompress the byte buffer back into the buffer using the specified * options. * * @param in * the bytes to decompress. * @param out * the buffer to fill with the decompressed data (must already be * allocated with enough space) * @param option * the options to use for decompressing. */ void decompress(ByteBuffer in, Buffer out, ICompressOption option); /** * @return a option instance that can be used to control the compression * meganism. */ ICompressOption option(); } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/api/package-info.java000066400000000000000000000035131476377620500313730ustar00rootroot00000000000000/** * Common interface definitions for the various compression algorithms (primarily for internal use) . The * compression classes of nom-tam-fits come in a very fragmented packaging, which is unfortunate -- both because it's * hard to get a sense of the overall organization and because it results in way too many internals being exposed to * users, which could have been resticted to package-level visibility for internal use only. Alas, that's what it is, so * it's going to stick... */ package nom.tam.fits.compression.algorithm.api; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/gzip/000077500000000000000000000000001476377620500264025ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/gzip/GZipCompressor.java000066400000000000000000000252301476377620500321750ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.gzip; import java.io.IOException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.DoubleBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.LongBuffer; import java.nio.ShortBuffer; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import nom.tam.fits.compression.algorithm.api.ICompressor; import nom.tam.util.ArrayFuncs; import nom.tam.util.ByteBufferInputStream; import nom.tam.util.ByteBufferOutputStream; import nom.tam.util.FitsIO; import nom.tam.util.type.ElementType; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * (for internal use) The GZIP compression algorithm. * * @param The genetic type of element buffer to compress */ @SuppressWarnings("javadoc") public abstract class GZipCompressor implements ICompressor { /** * Byte compress is a special case, the only one that does not extends GZipCompress because it can write the buffer * directly. */ public static class ByteGZipCompressor extends GZipCompressor { public ByteGZipCompressor() { super(1); nioBuffer = ByteBuffer.wrap(buffer); } @Override protected void getPixel(ByteBuffer pixelData, byte[] pixelBytes) { nioBuffer.put(pixelData); } @Override protected void setPixel(ByteBuffer pixelData, byte[] pixelBytes) { pixelData.put(nioBuffer); } } public static class DoubleGZipCompressor extends GZipCompressor { protected static final int BYTE_SIZE_OF_DOUBLE = 8; public DoubleGZipCompressor() { super(BYTE_SIZE_OF_DOUBLE); nioBuffer = ByteBuffer.wrap(buffer).asDoubleBuffer(); } @Override protected void getPixel(DoubleBuffer pixelData, byte[] pixelBytes) { nioBuffer.put(pixelData); } @Override protected void setPixel(DoubleBuffer pixelData, byte[] pixelBytes) { pixelData.put(nioBuffer); } } public static class FloatGZipCompressor extends GZipCompressor { protected static final int BYTE_SIZE_OF_FLOAT = 4; public FloatGZipCompressor() { super(BYTE_SIZE_OF_FLOAT); nioBuffer = ByteBuffer.wrap(buffer).asFloatBuffer(); } @Override protected void getPixel(FloatBuffer pixelData, byte[] pixelBytes) { nioBuffer.put(pixelData); } @Override protected void setPixel(FloatBuffer pixelData, byte[] pixelBytes) { pixelData.put(nioBuffer); } } public static class IntGZipCompressor extends GZipCompressor { protected static final int BYTE_SIZE_OF_INT = 4; public IntGZipCompressor() { super(BYTE_SIZE_OF_INT); nioBuffer = ByteBuffer.wrap(buffer).asIntBuffer(); } @Override protected void getPixel(IntBuffer pixelData, byte[] pixelBytes) { nioBuffer.put(pixelData); } @Override protected void setPixel(IntBuffer pixelData, byte[] pixelBytes) { pixelData.put(nioBuffer); } } public static class LongGZipCompressor extends GZipCompressor { protected static final int BYTE_SIZE_OF_LONG = 8; public LongGZipCompressor() { super(BYTE_SIZE_OF_LONG); nioBuffer = ByteBuffer.wrap(buffer).asLongBuffer(); } @Override protected void getPixel(LongBuffer pixelData, byte[] pixelBytes) { nioBuffer.put(pixelData); } @Override protected void setPixel(LongBuffer pixelData, byte[] pixelBytes) { pixelData.put(nioBuffer); } } public static class ShortGZipCompressor extends GZipCompressor { protected static final int BYTE_SIZE_OF_SHORT = 2; public ShortGZipCompressor() { super(BYTE_SIZE_OF_SHORT); nioBuffer = ByteBuffer.wrap(buffer).asShortBuffer(); } @Override protected void getPixel(ShortBuffer pixelData, byte[] pixelBytes) { nioBuffer.put(pixelData); } @Override protected void setPixel(ShortBuffer pixelData, byte[] pixelBytes) { pixelData.put(nioBuffer); } } private final class TypeConversion { private final ElementType from; private final ElementType to; private final B fromBuffer; private final T toBuffer; private final Object fromArray; private final Object toArray; private TypeConversion(ElementType from) { this.from = from; to = getElementType(GZipCompressor.this.primitiveSize); toBuffer = GZipCompressor.this.nioBuffer; fromBuffer = from.asTypedBuffer(ByteBuffer.wrap(GZipCompressor.this.buffer)); fromArray = from.newArray(DEFAULT_GZIP_BUFFER_SIZE / from.size()); toArray = to.newArray(DEFAULT_GZIP_BUFFER_SIZE / to.size()); } int copy(int byteCount) { fromBuffer.rewind(); toBuffer.rewind(); from.getArray(fromBuffer, fromArray); ArrayFuncs.copyInto(fromArray, toArray); to.putArray(toBuffer, toArray); return byteCount * to.size() / from.size(); } } private static final int DEFAULT_GZIP_BUFFER_SIZE = 65536; private static final int MINIMAL_GZIP_BUFFER_SIZE = 65536; protected final int primitiveSize; protected byte[] buffer = new byte[DEFAULT_GZIP_BUFFER_SIZE]; protected T nioBuffer; private final byte[] sizeArray = new byte[ElementType.INT.size()]; private final IntBuffer sizeBuffer = ByteBuffer.wrap(sizeArray).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer(); public GZipCompressor(int primitiveSize) { this.primitiveSize = primitiveSize; } @Override public boolean compress(T pixelData, ByteBuffer compressed) { nioBuffer.rewind(); int pixelDataLimit = pixelData.limit(); try (GZIPOutputStream zip = createGZipOutputStream(pixelDataLimit, compressed)) { while (pixelData.hasRemaining()) { int count = Math.min(pixelData.remaining(), nioBuffer.capacity()); pixelData.limit(pixelData.position() + count); getPixel(pixelData, null); zip.write(buffer, 0, nioBuffer.position() * primitiveSize); nioBuffer.rewind(); pixelData.limit(pixelDataLimit); } } catch (IOException e) { throw new IllegalStateException("could not gzip data", e); } compressed.limit(compressed.position()); return true; } @Override public void decompress(ByteBuffer compressed, T pixelData) { nioBuffer.rewind(); TypeConversion typeConverter = getTypeConverter(compressed, pixelData.limit()); try (GZIPInputStream zip = createGZipInputStream(compressed)) { int count; while ((count = zip.read(buffer)) >= 0) { if (typeConverter != null) { count = typeConverter.copy(count); } nioBuffer.position(0); nioBuffer.limit(count / primitiveSize); setPixel(pixelData, null); } } catch (IOException e) { throw new IllegalStateException("could not gunzip data", e); } } @SuppressWarnings("unchecked") private ElementType getElementType(int size) { return (ElementType) ElementType.forBitpix(size * FitsIO.BITS_OF_1_BYTE); } private TypeConversion getTypeConverter(ByteBuffer compressed, int nrOfPrimitiveElements) { if (compressed.limit() > FitsIO.BYTES_IN_INTEGER) { int oldPosition = compressed.position(); try { compressed.position(compressed.limit() - sizeArray.length); compressed.get(sizeArray); int uncompressedSize = sizeBuffer.get(0); if (uncompressedSize > 0) { compressed.position(oldPosition); if (uncompressedSize % nrOfPrimitiveElements == 0) { int compressedPrimitiveSize = uncompressedSize / nrOfPrimitiveElements; if (compressedPrimitiveSize != primitiveSize) { return new TypeConversion<>(getElementType(compressedPrimitiveSize)); } } } } finally { compressed.position(oldPosition); } } return null; } protected GZIPInputStream createGZipInputStream(ByteBuffer compressed) throws IOException { return new GZIPInputStream(new ByteBufferInputStream(compressed), Math.min(compressed.limit() * 2, DEFAULT_GZIP_BUFFER_SIZE)); } protected GZIPOutputStream createGZipOutputStream(int length, ByteBuffer compressed) throws IOException { return new GZIPOutputStream(new ByteBufferOutputStream(compressed), Math.min(Math.max(length * 2, MINIMAL_GZIP_BUFFER_SIZE), DEFAULT_GZIP_BUFFER_SIZE)); } protected abstract void getPixel(T pixelData, byte[] pixelBytes); protected abstract void setPixel(T pixelData, byte[] pixelBytes); } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/gzip/package-info.java000066400000000000000000000027411476377620500315750ustar00rootroot00000000000000/** *

* (for internal use) GZIP compression algorithms. It's weird that it's a package by itself, but that's life. *

*/ package nom.tam.fits.compression.algorithm.gzip; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/gzip2/000077500000000000000000000000001476377620500264645ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/gzip2/GZip2Compressor.java000066400000000000000000000175041476377620500323460ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.gzip2; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.io.IOException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.DoubleBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.LongBuffer; import java.nio.ShortBuffer; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import nom.tam.fits.compression.algorithm.gzip.GZipCompressor; import nom.tam.util.type.ElementType; /** * (for internal use) The GZIP2 compression algorithm. * * @param The genetic type of element buffer to compress */ @SuppressWarnings("javadoc") public abstract class GZip2Compressor extends GZipCompressor { public static class ByteGZip2Compressor extends ByteGZipCompressor { } public static class IntGZip2Compressor extends GZip2Compressor { public IntGZip2Compressor() { super(ElementType.INT.size()); } @Override protected void getPixel(IntBuffer pixelData, byte[] pixelBytes) { IntBuffer pixelBuffer = ByteBuffer.wrap(pixelBytes).asIntBuffer(); pixelBuffer.put(pixelData); } @Override protected void setPixel(IntBuffer pixelData, byte[] pixelBytes) { pixelData.put(ByteBuffer.wrap(pixelBytes).asIntBuffer()); } } public static class FloatGZip2Compressor extends GZip2Compressor { public FloatGZip2Compressor() { super(ElementType.FLOAT.size()); } @Override protected void getPixel(FloatBuffer pixelData, byte[] pixelBytes) { FloatBuffer pixelBuffer = ByteBuffer.wrap(pixelBytes).asFloatBuffer(); pixelBuffer.put(pixelData); } @Override protected void setPixel(FloatBuffer pixelData, byte[] pixelBytes) { pixelData.put(ByteBuffer.wrap(pixelBytes).asFloatBuffer()); } } public static class LongGZip2Compressor extends GZip2Compressor { public LongGZip2Compressor() { super(ElementType.LONG.size()); } @Override protected void getPixel(LongBuffer pixelData, byte[] pixelBytes) { LongBuffer pixelBuffer = ByteBuffer.wrap(pixelBytes).asLongBuffer(); pixelBuffer.put(pixelData); } @Override protected void setPixel(LongBuffer pixelData, byte[] pixelBytes) { pixelData.put(ByteBuffer.wrap(pixelBytes).asLongBuffer()); } } public static class DoubleGZip2Compressor extends GZip2Compressor { public DoubleGZip2Compressor() { super(ElementType.DOUBLE.size()); } @Override protected void getPixel(DoubleBuffer pixelData, byte[] pixelBytes) { DoubleBuffer pixelBuffer = ByteBuffer.wrap(pixelBytes).asDoubleBuffer(); pixelBuffer.put(pixelData); } @Override protected void setPixel(DoubleBuffer pixelData, byte[] pixelBytes) { pixelData.put(ByteBuffer.wrap(pixelBytes).asDoubleBuffer()); } } public static class ShortGZip2Compressor extends GZip2Compressor { public ShortGZip2Compressor() { super(ElementType.SHORT.size()); } @Override protected void getPixel(ShortBuffer pixelData, byte[] pixelBytes) { ShortBuffer shortBuffer = ByteBuffer.wrap(pixelBytes).asShortBuffer(); shortBuffer.put(pixelData); } @Override protected void setPixel(ShortBuffer pixelData, byte[] pixelBytes) { pixelData.put(ByteBuffer.wrap(pixelBytes).asShortBuffer()); } } public GZip2Compressor(int primitiveSize) { super(primitiveSize); } private int[] calculateOffsets(byte[] byteArray) { int[] offset = new int[primitiveSize]; offset[0] = 0; for (int primitivIndex = 1; primitivIndex < primitiveSize; primitivIndex++) { offset[primitivIndex] = offset[primitivIndex - 1] + byteArray.length / primitiveSize; } return offset; } @Override public boolean compress(T pixelData, ByteBuffer compressed) { int pixelDataLimit = pixelData.limit(); byte[] pixelBytes = new byte[pixelDataLimit * primitiveSize]; getPixel(pixelData, pixelBytes); pixelBytes = shuffle(pixelBytes); try (GZIPOutputStream zip = createGZipOutputStream(pixelDataLimit, compressed)) { // FIXME AK: FB complains the line below has a redundant null ckeck for 'zip', but where exactly? zip.write(pixelBytes, 0, pixelBytes.length); } catch (IOException e) { throw new IllegalStateException("could not gzip data", e); } return true; } @Override public void decompress(ByteBuffer compressed, T pixelData) { int pixelDataLimit = pixelData.limit(); byte[] pixelBytes = new byte[pixelDataLimit * primitiveSize]; try (GZIPInputStream zip = createGZipInputStream(compressed)) { int count = 0; int offset = 0; while (offset < pixelBytes.length && count >= 0) { count = zip.read(pixelBytes, offset, pixelBytes.length - offset); if (count >= 0) { offset = offset + count; } } } catch (IOException e) { throw new IllegalStateException("could not gunzip data", e); } pixelBytes = unshuffle(pixelBytes); setPixel(pixelData, pixelBytes); } public byte[] shuffle(byte[] byteArray) { byte[] result = new byte[byteArray.length]; int resultIndex = 0; int[] offset = calculateOffsets(byteArray); for (int index = 0; index < byteArray.length; index += primitiveSize) { for (int primitiveIndex = 0; primitiveIndex < primitiveSize; primitiveIndex++) { result[resultIndex + offset[primitiveIndex]] = byteArray[index + primitiveIndex]; } resultIndex++; } return result; } public byte[] unshuffle(byte[] byteArray) { byte[] result = new byte[byteArray.length]; int resultIndex = 0; int[] offset = calculateOffsets(byteArray); for (int index = 0; index < byteArray.length; index += primitiveSize) { for (int primitiveIndex = 0; primitiveIndex < primitiveSize; primitiveIndex++) { result[index + primitiveIndex] = byteArray[resultIndex + offset[primitiveIndex]]; } resultIndex++; } return result; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/gzip2/package-info.java000066400000000000000000000027441476377620500316620ustar00rootroot00000000000000/** *

* ( for internal use) GZIP2 compression algorithms. It's weird that it's a package by itself, but that's life. *

*/ package nom.tam.fits.compression.algorithm.gzip2; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/hcompress/000077500000000000000000000000001476377620500274345ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/hcompress/HCompress.java000066400000000000000000000670421476377620500322130ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.hcompress; import java.nio.ByteBuffer; import java.nio.LongBuffer; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** *

* (for internal use) A hierarchical data compression algoritm, used by the Hubble Data Archive and the STScI * Digital Sky Survey. *

*

* The original compression code was written by Richard White at the STScI and included (ported to c and adapted) in * cfitsio by William Pence, NASA/GSFC. That code was then ported to java by R. van Nieuwenhoven. Later it was massively * refactored to harmonize the different compression algorithms and reduce the duplicate code pieces without obscuring * the algorithm itself as far as possible. The original site for the algorithm is *

*

* See http://www.stsci.edu /software/hcompress.html *

* * @author Richard White * @author William Pence * @author Richard van Nieuwenhoven * * @see HDecompress * @see HCompressorOption */ @SuppressWarnings("javadoc") public class HCompress { private static final int HTRANS_START_MASK = -2; protected static final double ROUNDING_HALF = 0.5; protected static final int BITS_OF_1_BYTE = 8; protected static final int BITS_OF_1_NYBBLE = 4; protected static final int BYTE_MASK = 0xff; protected static final int NYBBLE_MASK = 0xF; /** * to be refactored to a good name. */ private static final int N3 = 3; private static final int[] BITS_MASK = {0, 1, 3, 7, 15, 31, 63, 127, 255}; /* * Huffman code values and number of bits in each code */ private static final int[] CODE = {0x3e, 0x00, 0x01, 0x08, 0x02, 0x09, 0x1a, 0x1b, 0x03, 0x1c, 0x0a, 0x1d, 0x0b, 0x1e, 0x3f, 0x0c}; private static final byte[] CODE_MAGIC = {(byte) 0xDD, (byte) 0x99}; private static final int[] NCODE = {6, 3, 3, 4, 3, 4, 5, 5, 3, 5, 4, 5, 4, 5, 6, 4}; /** * variables for bit output to buffer when Huffman coding */ private int bitbuffer; /** Number of bits free in buffer */ private int bitsToGo2; private int bitsToGo3; /** Bits buffered for output */ private int buffer2; private int b2i(boolean b) { return b ? 1 : 0; } private int bufcopy(byte[] a, int n, byte[] buffer, int b, long bmax) { int i; for (i = 0; i < n; i++) { if (a[i] != 0) { /* * add Huffman code for a[i] to buffer */ bitbuffer |= CODE[a[i]] << bitsToGo3; bitsToGo3 += NCODE[a[i]]; if (bitsToGo3 >= BITS_OF_1_BYTE) { buffer[b] = (byte) (bitbuffer & BYTE_MASK); b++; /* * return warning code if we fill buffer */ if (b >= bmax) { return b; } bitbuffer >>= BITS_OF_1_BYTE; bitsToGo3 -= BITS_OF_1_BYTE; } } } return b; } protected void compress(long[] aa, int ny, int nx, int scale, ByteBuffer output) { /* * compress the input image using the H-compress algorithm a - input image tiledImageOperation nx - size of X * axis of image ny - size of Y axis of image scale - quantization scale factor. Larger values results in more * (lossy) compression scale = 0 does lossless compression output - pre-allocated tiledImageOperation to hold * the output compressed stream of bytes nbyts - input value = size of the output buffer; returned value = size * of the compressed byte stream, in bytes NOTE: the nx and ny dimensions as defined within this code are * reversed from the usual FITS notation. ny is the fastest varying dimension, which is usually considered the X * axis in the FITS image display */ /* H-transform */ htrans(aa, nx, ny); LongBuffer a = LongBuffer.wrap(aa); /* digitize */ digitize(a, 0, nx, ny, scale); /* encode and write to output tiledImageOperation */ encode(output, a, nx, ny, scale); } private LongBuffer copy(LongBuffer a, int i) { a.position(i); return a.slice(); } private void digitize(LongBuffer a, int aOffset, int nx, int ny, long scale) { /* * round to multiple of scale */ if (scale <= 1) { return; } long d = (scale + 1L) / 2L - 1L; for (int index = 0; index < a.limit(); index++) { long current = a.get(index); a.put(index, (current > 0 ? current + d : current - d) / scale); } } /** * encode pixels. * * @param compressedBytes compressed data * @param pixels pixels to compress * @param nx image width dimension * @param ny image height dimension * @param nbitplanes Number of bit planes in quadrants */ private void doEncode(ByteBuffer compressedBytes, LongBuffer pixels, int nx, int ny, byte[] nbitplanes) { int nx2 = (nx + 1) / 2; int ny2 = (ny + 1) / 2; /* * Initialize bit output */ startOutputtingBits(); /* * write out the bit planes for each quadrant */ qtreeEncode(compressedBytes, copy(pixels, 0), ny, nx2, ny2, nbitplanes[0]); qtreeEncode(compressedBytes, copy(pixels, ny2), ny, nx2, ny / 2, nbitplanes[1]); qtreeEncode(compressedBytes, copy(pixels, ny * nx2), ny, nx / 2, ny2, nbitplanes[1]); qtreeEncode(compressedBytes, copy(pixels, ny * nx2 + ny2), ny, nx / 2, ny / 2, nbitplanes[2]); /* * Add zero as an EOF symbol */ outputNybble(compressedBytes, 0); doneOutputtingBits(compressedBytes); } private void doneOutputtingBits(ByteBuffer outfile) { if (bitsToGo2 < BITS_OF_1_BYTE) { /* putc(buffer2< 0) { /* * positive element, put zero at end of buffer */ signbits[nsign] <<= 1; bitsToGo--; } else if (a.get(i) < 0) { /* * negative element, shift in a one */ signbits[nsign] <<= 1; signbits[nsign] |= 1; bitsToGo--; /* * replace a by absolute value */ a.put(i, -a.get(i)); } if (bitsToGo == 0) { /* * filled up this byte, go to the next one */ bitsToGo = BITS_OF_1_BYTE; nsign++; signbits[nsign] = 0; } } if (bitsToGo != BITS_OF_1_BYTE) { /* * some bits in last element move bits in last byte to bottom and increment nsign */ signbits[nsign] <<= bitsToGo; nsign++; } /* * calculate number of bit planes for 3 quadrants quadrant 0=bottom left, 1=bottom right or top left, 2=top * right, */ for (int q = 0; q < N3; q++) { vmax[q] = 0; } /* * get maximum absolute value in each quadrant */ int nx2 = (nx + 1) / 2; int ny2 = (ny + 1) / 2; int j = 0; /* column counter */ int k = 0; /* row counter */ for (int i = 0; i < nel; i++) { int q = (j >= ny2 ? 1 : 0) + (k >= nx2 ? 1 : 0); if (vmax[q] < a.get(i)) { vmax[q] = a.get(i); } if (++j >= ny) { j = 0; k++; } } /* * now calculate number of bits for each quadrant */ /* this is a more efficient way to do this, */ for (int q = 0; q < N3; q++) { nbitplanes[q] = 0; while (vmax[q] > 0) { vmax[q] = vmax[q] >> 1; nbitplanes[q]++; } } /* * write nbitplanes */ compressedBytes.put(nbitplanes, 0, nbitplanes.length); /* * write coded tiledImageOperation */ doEncode(compressedBytes, a, nx, ny, nbitplanes); /* * write sign bits */ if (nsign > 0) { compressedBytes.put(signbits, 0, nsign); } return (int) noutchar; } private int htrans(long[] a, int nx, int ny) { /* * log2n is log2 of max(nx,ny) rounded up to next power of 2 */ int nmax = nx > ny ? nx : ny; int log2n = log2n(nmax); if (nmax > 1 << log2n) { log2n++; } /* * get temporary storage for shuffling elements */ long[] tmp = new long[(nmax + 1) / 2]; /* * set up rounding and shifting masks */ long shift = 0; long mask = HTRANS_START_MASK; long mask2 = mask << 1; long prnd = 1; long prnd2 = prnd << 1; long nrnd2 = prnd2 - 1; /* * do log2n reductions We're indexing a as a 2-D tiledImageOperation with dimensions (nx,ny). */ int nxtop = nx; int nytop = ny; for (int k = 0; k < log2n; k++) { int oddx = nxtop % 2; int oddy = nytop % 2; int i = 0; for (; i < nxtop - oddx; i += 2) { int s00 = i * ny; /* s00 is index of a[i,j] */ int s10 = s00 + ny; /* s10 is index of a[i+1,j] */ for (int j = 0; j < nytop - oddy; j += 2) { /* * Divide h0,hx,hy,hc by 2 (1 the first time through). */ long h0 = a[s10 + 1] + a[s10] + a[s00 + 1] + a[s00] >> shift; long hx = a[s10 + 1] + a[s10] - a[s00 + 1] - a[s00] >> shift; long hy = a[s10 + 1] - a[s10] + a[s00 + 1] - a[s00] >> shift; long hc = a[s10 + 1] - a[s10] - a[s00 + 1] + a[s00] >> shift; /* * Throw away the 2 bottom bits of h0, bottom bit of hx,hy. To get rounding to be same for positive * and negative numbers, nrnd2 = prnd2 - 1. */ a[s10 + 1] = hc; a[s10] = (hx >= 0 ? hx + prnd : hx) & mask; a[s00 + 1] = (hy >= 0 ? hy + prnd : hy) & mask; a[s00] = (h0 >= 0 ? h0 + prnd2 : h0 + nrnd2) & mask2; s00 += 2; s10 += 2; } if (oddy != 0) { /* * do last element in row if row length is odd s00+1, s10+1 are off edge */ long h0 = a[s10] + a[s00] << 1 - shift; long hx = a[s10] - a[s00] << 1 - shift; a[s10] = (hx >= 0 ? hx + prnd : hx) & mask; a[s00] = (h0 >= 0 ? h0 + prnd2 : h0 + nrnd2) & mask2; s00++; s10++; } } if (oddx != 0) { /* * do last row if column length is odd s10, s10+1 are off edge */ int s00 = i * ny; for (int j = 0; j < nytop - oddy; j += 2) { long h0 = a[s00 + 1] + a[s00] << 1 - shift; long hy = a[s00 + 1] - a[s00] << 1 - shift; a[s00 + 1] = (hy >= 0 ? hy + prnd : hy) & mask; a[s00] = (h0 >= 0 ? h0 + prnd2 : h0 + nrnd2) & mask2; s00 += 2; } if (oddy != 0) { /* * do corner element if both row and column lengths are odd s00+1, s10, s10+1 are off edge */ long h0 = a[s00] << 2 - shift; a[s00] = (h0 >= 0 ? h0 + prnd2 : h0 + nrnd2) & mask2; } } /* * now shuffle in each dimension to group coefficients by order */ // achtung eigenlich pointer nach a for (i = 0; i < nxtop; i++) { shuffle(a, ny * i, nytop, 1, tmp); } for (int j = 0; j < nytop; j++) { shuffle(a, j, nxtop, ny, tmp); } /* * image size reduced by 2 (round up if odd) */ nxtop = nxtop + 1 >> 1; nytop = nytop + 1 >> 1; /* * divisor doubles after first reduction */ shift = 1; /* * masks, rounding values double after each iteration */ mask = mask2; prnd = prnd2; mask2 = mask2 << 1; prnd2 = prnd2 << 1; nrnd2 = prnd2 - 1; } return 0; } private int log2n(int nqmax) { return (int) (Math.log(nqmax) / Math.log(2.0) + ROUNDING_HALF); } private void outputNbits(ByteBuffer outfile, int bits, int n) { /* AND mask for the right-most n bits */ /* * insert bits at end of buffer */ buffer2 <<= n; /* buffer2 |= ( bits & ((1<> -bitsToGo2 & BYTE_MASK)); bitsToGo2 += BITS_OF_1_BYTE; } } private void outputNnybble(ByteBuffer outfile, int n, byte[] array) { /* * pack the 4 lower bits in each element of the tiledImageOperation into the outfile tiledImageOperation */ int ii, jj, kk = 0, shift; if (n == 1) { outputNybble(outfile, array[0]); return; } /* * forcing byte alignment doesn;t help, and even makes it go slightly slower if (bits_to_go2 != 8) * output_nbits(outfile, kk, bits_to_go2); */ if (bitsToGo2 <= BITS_OF_1_NYBBLE) { /* just room for 1 nybble; write it out separately */ outputNybble(outfile, array[0]); kk++; /* index to next tiledImageOperation element */ if (n == 2) { // only 1 more nybble to write out outputNybble(outfile, array[1]); return; } } /* bits_to_go2 is now in the range 5 - 8 */ shift = BITS_OF_1_BYTE - bitsToGo2; /* * now write out pairs of nybbles; this does not affect value of bits_to_go2 */ jj = (n - kk) / 2; if (bitsToGo2 == BITS_OF_1_BYTE) { /* special case if nybbles are aligned on byte boundary */ /* this actually seems to make very little difference in speed */ buffer2 = 0; for (ii = 0; ii < jj; ii++) { outfile.put((byte) ((array[kk] & NYBBLE_MASK) << BITS_OF_1_NYBBLE | array[kk + 1] & NYBBLE_MASK)); kk += 2; } } else { for (ii = 0; ii < jj; ii++) { buffer2 = buffer2 << BITS_OF_1_BYTE | (array[kk] & NYBBLE_MASK) << BITS_OF_1_NYBBLE | array[kk + 1] & NYBBLE_MASK; kk += 2; /* * buffer2 full, put out top 8 bits */ outfile.put((byte) (buffer2 >> shift & BYTE_MASK)); } } /* write out last odd nybble, if present */ if (kk != n) { outputNybble(outfile, array[n - 1]); } } private void outputNybble(ByteBuffer outfile, int bits) { /* * insert 4 bits at end of buffer */ buffer2 = buffer2 << BITS_OF_1_NYBBLE | bits & NYBBLE_MASK; bitsToGo2 -= BITS_OF_1_NYBBLE; if (bitsToGo2 <= 0) { /* * buffer2 full, put out top 8 bits */ outfile.put((byte) (buffer2 >> -bitsToGo2 & BYTE_MASK)); bitsToGo2 += BITS_OF_1_BYTE; } } /** * macros to write out 4-bit nybble, Huffman code for this value */ private int qtreeEncode(ByteBuffer outfile, LongBuffer a, int n, int nqx, int nqy, int nbitplanes) { /* * int a[]; int n; physical dimension of row in a int nqx; length of row int nqy; length of column (<=n) int * nbitplanes; number of bit planes to output */ int log2n, i, k, bit, b, nqmax, nqx2, nqy2, nx, ny; long bmax; byte[] scratch, buffer; /* * log2n is log2 of max(nqx,nqy) rounded up to next power of 2 */ nqmax = nqx > nqy ? nqx : nqy; log2n = log2n(nqmax); if (nqmax > 1 << log2n) { log2n++; } /* * initialize buffer point, max buffer size */ nqx2 = (nqx + 1) / 2; nqy2 = (nqy + 1) / 2; bmax = (nqx2 * nqy2 + 1) / 2; /* * We're indexing A as a 2-D tiledImageOperation with dimensions (nqx,nqy). Scratch is 2-D with dimensions * (nqx/2,nqy/2) rounded up. Buffer is used to store string of codes for output. */ scratch = new byte[(int) (2 * bmax)]; buffer = new byte[(int) bmax]; /* * now encode each bit plane, starting with the top */ bitplane_done: for (bit = nbitplanes - 1; bit >= 0; bit--) { /* * initial bit buffer */ b = 0; bitbuffer = 0; bitsToGo3 = 0; /* * on first pass copy A to scratch tiledImageOperation */ qtreeOnebit(a, n, nqx, nqy, scratch, bit); nx = nqx + 1 >> 1; ny = nqy + 1 >> 1; /* * copy non-zero values to output buffer, which will be written in reverse order */ b = bufcopy(scratch, nx * ny, buffer, b, bmax); if (b >= bmax) { /* * quadtree is expanding data, change warning code and just fill buffer with bit-map */ writeBdirect(outfile, a, n, nqx, nqy, scratch, bit); continue bitplane_done; } /* * do log2n reductions */ for (k = 1; k < log2n; k++) { qtreeReduce(scratch, ny, nx, ny, scratch); nx = nx + 1 >> 1; ny = ny + 1 >> 1; b = bufcopy(scratch, nx * ny, buffer, b, bmax); if (b >= bmax) { writeBdirect(outfile, a, n, nqx, nqy, scratch, bit); continue bitplane_done; } } /* * OK, we've got the code in buffer Write quadtree warning code, then write buffer in reverse order */ outputNybble(outfile, NYBBLE_MASK); if (b == 0) { if (bitsToGo3 > 0) { /* * put out the last few bits */ outputNbits(outfile, bitbuffer & (1 << bitsToGo3) - 1, bitsToGo3); } else { /* * have to write a zero nybble if there are no 1's in tiledImageOperation */ outputNbits(outfile, CODE[0], NCODE[0]); } } else { if (bitsToGo3 > 0) { /* * put out the last few bits */ outputNbits(outfile, bitbuffer & (1 << bitsToGo3) - 1, bitsToGo3); } for (i = b - 1; i >= 0; i--) { outputNbits(outfile, buffer[i], BITS_OF_1_BYTE); } } } return 0; } private void qtreeOnebit(LongBuffer a, int n, int nx, int ny, byte[] b, int bit) { int i, j, k; long b0, b1, b2, b3; int s10, s00; /* * use selected bit to get amount to shift */ b0 = 1L << bit; b1 = b0 << 1; b2 = b1 << 1; b3 = b2 << 1; k = 0; /* k is index of b[i/2,j/2] */ for (i = 0; i < nx - 1; i += 2) { s00 = n * i; /* s00 is index of a[i,j] */ /* * tried using s00+n directly in the statements, but this had no effect on performance */ s10 = s00 + n; /* s10 is index of a[i+1,j] */ for (j = 0; j < ny - 1; j += 2) { b[k] = (byte) ((a.get(s10 + 1) & b0 // | a.get(s10) << 1 & b1 // | a.get(s00 + 1) << 2 & b2 // | a.get(s00) << N3 & b3) >> bit); k++; s00 += 2; s10 += 2; } if (j < ny) { /* * row size is odd, do last element in row s00+1,s10+1 are off edge */ b[k] = (byte) ((a.get(s10) << 1 & b1 | a.get(s00) << N3 & b3) >> bit); k++; } } if (i < nx) { /* * column size is odd, do last row s10,s10+1 are off edge */ s00 = n * i; for (j = 0; j < ny - 1; j += 2) { b[k] = (byte) ((a.get(s00 + 1) << 2 & b2 | a.get(s00) << N3 & b3) >> bit); k++; s00 += 2; } if (j < ny) { /* * both row and column size are odd, do corner element s00+1, s10, s10+1 are off edge */ b[k] = (byte) ((a.get(s00) << N3 & b3) >> bit); k++; } } } private void qtreeReduce(byte[] a, int n, int nx, int ny, byte[] b) { int i, j, k; int s10, s00; k = 0; /* k is index of b[i/2,j/2] */ for (i = 0; i < nx - 1; i += 2) { s00 = n * i; /* s00 is index of a[i,j] */ s10 = s00 + n; /* s10 is index of a[i+1,j] */ for (j = 0; j < ny - 1; j += 2) { b[k] = (byte) (b2i(a[s10 + 1] != 0) | b2i(a[s10] != 0) << 1 | b2i(a[s00 + 1] != 0) << 2 | b2i(a[s00] != 0) << N3); k++; s00 += 2; s10 += 2; } if (j < ny) { /* * row size is odd, do last element in row s00+1,s10+1 are off edge */ b[k] = (byte) (b2i(a[s10] != 0) << 1 | b2i(a[s00] != 0) << N3); k++; } } if (i < nx) { /* * column size is odd, do last row s10,s10+1 are off edge */ s00 = n * i; for (j = 0; j < ny - 1; j += 2) { b[k] = (byte) (b2i(a[s00 + 1] != 0) << 2 | b2i(a[s00] != 0) << N3); k++; s00 += 2; } if (j < ny) { /* * both row and column size are odd, do corner element s00+1, s10, s10+1 are off edge */ b[k] = (byte) (b2i(a[s00] != 0) << N3); k++; } } } private void shuffle(long[] a, int aOffset, int n, int n2, long[] tmp) { /* * int a[]; tiledImageOperation to shuffle int n; number of elements to shuffle int n2; second dimension int * tmp[]; scratch storage */ int i; long[] p1, p2, pt; int p1Offset; int ptOffset; int p2Offset; /* * copy odd elements to tmp */ pt = tmp; ptOffset = 0; p1 = a; p1Offset = aOffset + n2; for (i = 1; i < n; i += 2) { pt[ptOffset] = p1[p1Offset]; ptOffset++; p1Offset += n2 + n2; } /* * compress even elements into first half of A */ p1 = a; p1Offset = aOffset + n2; p2 = a; p2Offset = aOffset + n2 + n2; for (i = 2; i < n; i += 2) { p1[p1Offset] = p2[p2Offset]; p1Offset += n2; p2Offset += n2 + n2; } /* * put odd elements into 2nd half */ pt = tmp; ptOffset = 0; for (i = 1; i < n; i += 2) { p1[p1Offset] = pt[ptOffset]; p1Offset += n2; ptOffset++; } } private void startOutputtingBits() { buffer2 = 0; /* Buffer is empty to start */ bitsToGo2 = BITS_OF_1_BYTE; /* with */ } private void writeBdirect(ByteBuffer outfile, LongBuffer a, int n, int nqx, int nqy, byte[] scratch, int bit) { /* * Write the direct bitmap warning code */ outputNybble(outfile, 0x0); /* * Copy A to scratch tiledImageOperation (again!), packing 4 bits/nybble */ qtreeOnebit(a, n, nqx, nqy, scratch, bit); /* * write to outfile */ /* * int i; for (i = 0; i < ((nqx+1)/2) * ((nqy+1)/2); i++) { output_nybble(outfile,scratch[i]); } */ outputNnybble(outfile, (nqx + 1) / 2 * ((nqy + 1) / 2), scratch); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/hcompress/HCompressor.java000066400000000000000000000157621476377620500325560ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.hcompress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; import nom.tam.fits.compression.algorithm.api.ICompressor; import nom.tam.fits.compression.algorithm.quant.QuantizeProcessor.DoubleQuantCompressor; import nom.tam.fits.compression.algorithm.quant.QuantizeProcessor.FloatQuantCompressor; import nom.tam.util.ArrayFuncs; /** * (for internal use) Data compressor using the HCompress algorithm. * * @param The generic type of buffer that accessed the type of elements needed for the compression */ @SuppressWarnings("javadoc") public abstract class HCompressor implements ICompressor { public static class ByteHCompressor extends HCompressor { private static final long BYTE_MASK_FOR_LONG = 0xFFL; /** * HCompress of byte streams with the default scale parameter of 0 and no smoothing (lossless compression). * * @since 1.19.1 * * @author Attila Kovacs */ public ByteHCompressor() { this(new HCompressorOption()); } public ByteHCompressor(HCompressorOption options) { super(options); } @Override public boolean compress(ByteBuffer buffer, ByteBuffer compressed) { byte[] byteArray = new byte[buffer.limit()]; buffer.get(byteArray); long[] longArray = new long[byteArray.length]; for (int index = 0; index < longArray.length; index++) { longArray[index] = byteArray[index] & BYTE_MASK_FOR_LONG; } compress(longArray, compressed); return true; } @Override public void decompress(ByteBuffer compressed, ByteBuffer buffer) { long[] longArray = new long[buffer.limit()]; decompress(compressed, longArray); for (long element : longArray) { buffer.put((byte) element); } } } public static class DoubleHCompressor extends DoubleQuantCompressor { @SuppressWarnings("deprecation") public DoubleHCompressor(HCompressorQuantizeOption options) { super(options, new IntHCompressor((HCompressorOption) options.getCompressOption())); } } public static class FloatHCompressor extends FloatQuantCompressor { @SuppressWarnings("deprecation") public FloatHCompressor(HCompressorQuantizeOption options) { super(options, new IntHCompressor((HCompressorOption) options.getCompressOption())); } } public static class IntHCompressor extends HCompressor { /** * HCompress of 32-bit integer streams with the default scale parameter of 0 and no smoothing for lossless * compression. * * @since 1.19.1 * * @author Attila Kovacs */ public IntHCompressor() { this(new HCompressorOption()); } public IntHCompressor(HCompressorOption options) { super(options); } @Override public boolean compress(IntBuffer buffer, ByteBuffer compressed) { int[] intArray = new int[buffer.limit()]; buffer.get(intArray); long[] longArray = new long[intArray.length]; ArrayFuncs.copyInto(intArray, longArray); compress(longArray, compressed); return true; } @Override public void decompress(ByteBuffer compressed, IntBuffer buffer) { long[] longArray = new long[buffer.limit()]; decompress(compressed, longArray); for (long element : longArray) { buffer.put((int) element); } } } public static class ShortHCompressor extends HCompressor { /** * HCompress of 16-bit integer streams with the default scale parameter of 0 (lossless compression). * * @since 1.19.1 * * @author Attila Kovacs */ public ShortHCompressor() { this(new HCompressorOption()); } public ShortHCompressor(HCompressorOption options) { super(options); } @Override public boolean compress(ShortBuffer buffer, ByteBuffer compressed) { short[] shortArray = new short[buffer.limit()]; buffer.get(shortArray); long[] longArray = new long[shortArray.length]; ArrayFuncs.copyInto(shortArray, longArray); compress(longArray, compressed); return true; } @Override public void decompress(ByteBuffer compressed, ShortBuffer buffer) { long[] longArray = new long[buffer.limit()]; decompress(compressed, longArray); for (long element : longArray) { buffer.put((short) element); } } } private final HCompress compress; private final HDecompress decompress; private final HCompressorOption options; public HCompressor(HCompressorOption options) { this.options = options; compress = new HCompress(); decompress = new HDecompress(); } private HCompress compress() { return compress; } protected void compress(long[] longArray, ByteBuffer compressed) { compress().compress(longArray, options.getTileHeight(), options.getTileWidth(), options.getScale(), compressed); } private HDecompress decompress() { return decompress; } protected void decompress(ByteBuffer compressed, long[] aa) { decompress().decompress(compressed, options.isSmooth(), aa); } } HCompressorOption.java000066400000000000000000000134631476377620500336640ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/hcompresspackage nom.tam.fits.compression.algorithm.hcompress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.compression.algorithm.api.ICompressOption; import nom.tam.fits.compression.provider.param.api.ICompressParameters; import nom.tam.fits.compression.provider.param.hcompress.HCompressParameters; /** * Options to the HCompress compression algorithm. When compressing tables and images using the HCompress algorithm, * users can control how exactly the compression is perfomed. When reading compressed FITS files, these options will be * set automatically based on the header values recorded in the compressed HDU. * * @see nom.tam.image.compression.hdu.CompressedImageHDU#setCompressAlgorithm(String) * @see nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class) */ public class HCompressorOption implements ICompressOption { /** Shared configuration across copies. */ private final Config config; /** The parameters that represent settings for this option in the FITS headers and/or compressed data columns */ private HCompressParameters parameters; private int tileHeight; private int tileWidth; /** * Creates a new set of options for HCompress. */ public HCompressorOption() { config = new Config(); setParameters(new HCompressParameters(this)); } @Override public HCompressorOption copy() { try { HCompressorOption copy = (HCompressorOption) clone(); copy.parameters = parameters.copy(copy); return copy; } catch (CloneNotSupportedException e) { throw new IllegalStateException("option could not be cloned", e); } } @Override public HCompressParameters getCompressionParameters() { return parameters; } /** * Returns the scale parameter value * * @return the value of the scale parameter. * * @see #setScale(double) */ public int getScale() { return config.scale; } @Override public int getTileHeight() { return tileHeight; } @Override public int getTileWidth() { return tileWidth; } @Override public boolean isLossyCompression() { return config.scale > 0 || config.smooth; } /** * Checks if smoothing is enabled * * @return true if smoothing is enabled, otherwise false. * * @see #setSmooth(boolean) */ public boolean isSmooth() { return config.smooth; } @Override public void setParameters(ICompressParameters parameters) { if (!(parameters instanceof HCompressParameters)) { throw new IllegalArgumentException("Wrong type of parameters: " + parameters.getClass().getName()); } this.parameters = (HCompressParameters) parameters; } /** * Sets the scale parameter * * @param value the new scale parameter, which will be rounded to the nearest integer value for * the actual implementation. * * @return itself * * @throws IllegalArgumentException if the scale value is negative * * @see #getScale() */ public HCompressorOption setScale(double value) throws IllegalArgumentException { if (value < 0.0) { throw new IllegalArgumentException("Scale value cannot be negative: " + value); } config.scale = (int) Math.round(value); return this; } /** * Enabled or disables smoothing. * * @param value true to enable smoothing, or false to disable. * * @return itself */ public HCompressorOption setSmooth(boolean value) { config.smooth = value; return this; } @Override public T unwrap(Class clazz) { if (clazz.isAssignableFrom(this.getClass())) { return clazz.cast(this); } return null; } @Override public HCompressorOption setTileHeight(int value) { tileHeight = value; return this; } @Override public HCompressorOption setTileWidth(int value) { tileWidth = value; return this; } /** * Stores configuration in a way that can be shared and modified across enclosing option copies. * * @author Attila Kovacs * * @since 1.18 */ private static final class Config { private int scale; private boolean smooth; } } HCompressorQuantizeOption.java000066400000000000000000000063221476377620500354010ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/hcompresspackage nom.tam.fits.compression.algorithm.hcompress; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.compression.algorithm.quant.QuantizeOption; /** * @deprecated (for internal use) This class should not be exposed to users. *

* Options to the HCompress compression algorithm when the compression includes quantization. When * compressing tables and images using the HCompress algorithm, including quantization, users can * control how exactly the compression and quantization are perfomed. When reading compressed FITS * files, these options will be set automatically based on the header values recorded in the compressed * HDU. *

* * @see nom.tam.image.compression.hdu.CompressedImageHDU#setCompressAlgorithm(String) * @see nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class) * @see HCompressorOption */ @Deprecated public class HCompressorQuantizeOption extends QuantizeOption { /** * Creates a new set of options for HCompress with quantization, initialized to default values. */ public HCompressorQuantizeOption() { super(new HCompressorOption()); } /** * Creates a new set of options for HCompress with quantization, using the specified option to HCompress, and * initializing the qunatization options with default values. * * @param compressOption The HCompress options to use */ public HCompressorQuantizeOption(HCompressorOption compressOption) { super(compressOption); } /** * Returns the options that are specific to the HCompress algorithm (without quantization). * * @return the included options to the HCompress algorithm * * @see #getCompressOption(Class) */ public HCompressorOption getHCompressorOption() { return (HCompressorOption) getCompressOption(); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/hcompress/HDecompress.java000066400000000000000000001131021476377620500325110ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.hcompress; import java.nio.ByteBuffer; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import static nom.tam.fits.compression.algorithm.hcompress.HCompress.BITS_OF_1_BYTE; import static nom.tam.fits.compression.algorithm.hcompress.HCompress.BITS_OF_1_NYBBLE; import static nom.tam.fits.compression.algorithm.hcompress.HCompress.BYTE_MASK; import static nom.tam.fits.compression.algorithm.hcompress.HCompress.NYBBLE_MASK; import static nom.tam.fits.compression.algorithm.hcompress.HCompress.ROUNDING_HALF; /** * (for internal use) A hierarchical data decompression algoritm, e.g. for the Hubble Data Archive and the STScI * Digital Sky Survey. *

* The original decompression code was written by R. White at the STScI and included (ported to c and adapted) in * cfitsio by William Pence, NASA/GSFC. That code was then ported to java by R. van Nieuwenhoven. Later it was massively * refactored to harmonize the different compression algorithms and reduce the duplicate code pieces without obscuring * the algorithm itself as far as possible. The original site for the algorithm is *

*

* See http://www.stsci.edu/software/hcompress.html *

* * @author Richard White * @author William Pence * @author Richard van Nieuwenhoven */ public class HDecompress { private static class LongArrayPointer { private final long[] a; private int offset; LongArrayPointer(long[] tmp) { a = tmp; offset = 0; } public void bitOr(int i, long planeVal) { a[offset + i] |= planeVal; } public LongArrayPointer copy(int extraOffset) { LongArrayPointer intAP = new LongArrayPointer(a); intAP.offset = offset + extraOffset; return intAP; } public long get() { return a[offset]; } public long get(int i) { return a[offset + i]; } public void set(int i, long value) { a[offset + i] = value; } public void set(long value) { a[offset] = value; } } private static final byte[] CODE_MAGIC = {(byte) 0xDD, (byte) 0x99}; private static final int[] MASKS = {0, 1, 3, 7, 15, 31, 63, 127, 255}; private static final byte ZERO = 0; private static final byte BIT_ONE = 1; private static final byte BIT_TWO = 2; private static final byte BIT_THREE = 4; private static final byte BIT_FOUR = 8; /** * these N constants are obscuring the algorithm and should get some explaining javadoc if somebody understands the * algorithm. */ private static final int N03 = 3; private static final int N04 = 4; private static final int N05 = 5; private static final int N06 = 6; private static final int N07 = 7; private static final int N08 = 8; private static final int N09 = 9; private static final int N10 = 10; private static final int N11 = 11; private static final int N12 = 12; private static final int N13 = 13; private static final int N14 = 14; private static final int N15 = 15; private static final int N26 = 26; private static final int N27 = 27; private static final int N28 = 28; private static final int N29 = 29; private static final int N30 = 30; private static final int N31 = 31; private static final int N62 = 62; private static final int N63 = 63; /** * Number of bits still in buffer */ private int bitsToGo; /** Bits waiting to be input */ private int buffer2; private int nx; private int ny; private int scale; /** * log2n is log2 of max(nx,ny) rounded up to next power of 2 */ private int calculateLog2N(int nmax) { int log2n; log2n = (int) (Math.log(nmax) / Math.log(2.0) + ROUNDING_HALF); if (nmax > 1 << log2n) { log2n++; } return log2n; } /** * char *infile; input file long *a; address of output tiledImageOperation [nx][ny] int *nx,*ny; size of output * tiledImageOperation int *scale; scale factor for digitization * * @param infile * @param a */ private void decode64(ByteBuffer infile, LongArrayPointer a) { byte[] nbitplanes = new byte[N03]; byte[] tmagic = new byte[2]; /* * File starts either with special 2-byte magic code or with FITS keyword "SIMPLE =" */ infile.get(tmagic); /* * check for correct magic code value */ if (tmagic[0] != CODE_MAGIC[0] || tmagic[1] != CODE_MAGIC[1]) { throw new RuntimeException("Compression error"); } nx = infile.getInt(); /* x size of image */ ny = infile.getInt(); /* y size of image */ scale = infile.getInt(); /* scale factor for digitization */ /* sum of all pixels */ long sumall = infile.getLong(); /* # bits in quadrants */ infile.get(nbitplanes); dodecode64(infile, a, nbitplanes); /* * put sum of all pixels back into pixel 0 */ a.set(0, sumall); } /** * decompress the input byte stream using the H-compress algorithm input - input tiledImageOperation of compressed * bytes a - pre-allocated tiledImageOperation to hold the output uncompressed image nx - returned X axis size ny - * returned Y axis size NOTE: the nx and ny dimensions as defined within this code are reversed from the usual FITS * notation. ny is the fastest varying dimension, which is usually considered the X axis in the FITS image display * * @param input the input buffer to decompress * @param smooth should the image be smoothed * @param aa the resulting long tiledImageOperation */ public void decompress(ByteBuffer input, boolean smooth, long[] aa) { LongArrayPointer a = new LongArrayPointer(aa); /* decode the input tiledImageOperation */ decode64(input, a); /* * Un-Digitize */ undigitize64(a); /* * Inverse H-transform */ hinv64(a, smooth); } /** * long a[]; int nx,ny; Array dimensions are [nx][ny] unsigned char nbitplanes[3]; Number of bit planes in quadrants */ private int dodecode64(ByteBuffer infile, LongArrayPointer a, byte[] nbitplanes) { int nel = nx * ny; int nx2 = (nx + 1) / 2; int ny2 = (ny + 1) / 2; /* * initialize a to zero */ for (int i = 0; i < nel; i++) { a.set(i, 0); } /* * Initialize bit input */ startInputingBits(); /* * read bit planes for each quadrant */ qtreeDecode64(infile, a.copy(0), ny, nx2, ny2, nbitplanes[0]); qtreeDecode64(infile, a.copy(ny2), ny, nx2, ny / 2, nbitplanes[1]); qtreeDecode64(infile, a.copy(ny * nx2), ny, nx / 2, ny2, nbitplanes[1]); qtreeDecode64(infile, a.copy(ny * nx2 + ny2), ny, nx / 2, ny / 2, nbitplanes[2]); /* * make sure there is an EOF symbol (nybble=0) at end */ if (inputNybble(infile) != 0) { throw new RuntimeException("Compression error"); } /* * now get the sign bits Re-initialize bit input */ startInputingBits(); for (int i = 0; i < nel; i++) { if (a.get(i) != 0) { if (inputBit(infile) != 0) { a.set(i, -a.get(i)); } } } return 0; } /** * int smooth; 0 for no smoothing, else smooth during inversion int scale; used if smoothing is specified */ private int hinv64(LongArrayPointer a, boolean smooth) { int nmax = nx > ny ? nx : ny; int log2n = calculateLog2N(nmax); // get temporary storage for shuffling elements long[] tmp = new long[(nmax + 1) / 2]; // set up masks, rounding parameters int shift = 1; long bit0 = (long) 1 << log2n - 1; long bit1 = bit0 << 1; long bit2 = bit0 << 2; long mask0 = -bit0; long mask1 = mask0 << 1; long mask2 = mask0 << 2; long prnd0 = bit0 >> 1; long prnd1 = bit1 >> 1; long prnd2 = bit2 >> 1; long nrnd0 = prnd0 - 1; long nrnd1 = prnd1 - 1; long nrnd2 = prnd2 - 1; // round h0 to multiple of bit2 a.set(0, a.get(0) + (a.get(0) >= 0 ? prnd2 : nrnd2) & mask2); // do log2n expansions We're indexing a as a 2-D tiledImageOperation // with dimensions // (nx,ny). int nxtop = 1; int nytop = 1; int nxf = nx; int nyf = ny; int c = 1 << log2n; int i; for (int k = log2n - 1; k >= 0; k--) { // this somewhat cryptic code generates the sequence ntop[k-1] = // (ntop[k]+1)/2, where ntop[log2n] = n c = c >> 1; nxtop = nxtop << 1; nytop = nytop << 1; if (nxf <= c) { nxtop--; } else { nxf -= c; } if (nyf <= c) { nytop--; } else { nyf -= c; } // double shift and fix nrnd0 (because prnd0=0) on last pass if (k == 0) { nrnd0 = 0; shift = 2; } // unshuffle in each dimension to interleave coefficients for (i = 0; i < nxtop; i++) { unshuffle64(a.copy(ny * i), nytop, 1, tmp); } for (int j = 0; j < nytop; j++) { unshuffle64(a.copy(j), nxtop, ny, tmp); } // smooth by interpolating coefficients if SMOOTH != 0 if (smooth) { hsmooth64(a, nxtop, nytop); } int oddx = nxtop % 2; int oddy = nytop % 2; for (i = 0; i < nxtop - oddx; i += 2) { int s00 = ny * i; /* s00 is index of a[i,j] */ int s10 = s00 + ny; /* s10 is index of a[i+1,j] */ for (int j = 0; j < nytop - oddy; j += 2) { long h0 = a.get(s00); long hx = a.get(s10); long hy = a.get(s00 + 1); long hc = a.get(s10 + 1); // round hx and hy to multiple of bit1, hc to multiple of // bit0 h0 is already a multiple of bit2 hx = hx + (hx >= 0 ? prnd1 : nrnd1) & mask1; hy = hy + (hy >= 0 ? prnd1 : nrnd1) & mask1; hc = hc + (hc >= 0 ? prnd0 : nrnd0) & mask0; // propagate bit0 of hc to hx,hy long lowbit0 = hc & bit0; hx = hx >= 0 ? hx - lowbit0 : hx + lowbit0; hy = hy >= 0 ? hy - lowbit0 : hy + lowbit0; // Propagate bits 0 and 1 of hc,hx,hy to h0. This could be // simplified if we assume h0>0, but then the inversion // would not be lossless for images with negative pixels. long lowbit1 = (hc ^ hx ^ hy) & bit1; h0 = h0 >= 0 ? h0 + lowbit0 - lowbit1 : h0 + (lowbit0 == 0 ? lowbit1 : lowbit0 - lowbit1); // Divide sums by 2 (4 last time) a.set(s10 + 1, h0 + hx + hy + hc >> shift); a.set(s10, h0 + hx - hy - hc >> shift); a.set(s00 + 1, h0 - hx + hy - hc >> shift); a.set(s00, h0 - hx - hy + hc >> shift); s00 += 2; s10 += 2; } if (oddy != 0) { // do last element in row if row length is odd s00+1, s10+1 // are off edge long h0 = a.get(s00); long hx = a.get(s10); hx = (hx >= 0 ? hx + prnd1 : hx + nrnd1) & mask1; long lowbit1 = hx & bit1; h0 = h0 >= 0 ? h0 - lowbit1 : h0 + lowbit1; a.set(s10, h0 + hx >> shift); a.set(s00, h0 - hx >> shift); } } if (oddx != 0) { // do last row if column length is odd s10, s10+1 are off edge int s00 = ny * i; for (int j = 0; j < nytop - oddy; j += 2) { long h0 = a.get(s00); long hy = a.get(s00 + 1); hy = (hy >= 0 ? hy + prnd1 : hy + nrnd1) & mask1; long lowbit1 = hy & bit1; h0 = h0 >= 0 ? h0 - lowbit1 : h0 + lowbit1; a.set(s00 + 1, h0 + hy >> shift); a.set(s00, h0 - hy >> shift); s00 += 2; } if (oddy != 0) { // do corner element if both row and column lengths are odd // s00+1, s10, s10+1 are off edge long h0 = a.get(s00); a.set(s00, h0 >> shift); } } // divide all the masks and rounding values by 2 bit1 = bit0; bit0 = bit0 >> 1; mask1 = mask0; mask0 = mask0 >> 1; prnd1 = prnd0; prnd0 = prnd0 >> 1; nrnd1 = nrnd0; nrnd0 = prnd0 - 1; } return 0; } /** * long a[]; tiledImageOperation of H-transform coefficients int nxtop,nytop; size of coefficient block to use int * ny; actual 1st dimension of tiledImageOperation int scale; truncation scale factor that was used */ private void hsmooth64(LongArrayPointer a, int nxtop, int nytop) { int i, j; int ny2, s10, s00; long hm, h0, hp, hmm, hpm, hmp, hpp, hx2, hy2, diff, dmax, dmin, s, smax, m1, m2; /* * Maximum change in coefficients is determined by scale factor. Since we rounded during division (see * digitize.c), the biggest permitted change is scale/2. */ smax = scale >> 1; if (smax <= 0) { return; } ny2 = ny << 1; /* * We're indexing a as a 2-D tiledImageOperation with dimensions (nxtop,ny) of which only (nxtop,nytop) are * used. The coefficients on the edge of the tiledImageOperation are not adjusted (which is why the loops below * start at 2 instead of 0 and end at nxtop-2 instead of nxtop.) */ /* * Adjust x difference hx */ for (i = 2; i < nxtop - 2; i += 2) { s00 = ny * i; /* s00 is index of a[i,j] */ s10 = s00 + ny; /* s10 is index of a[i+1,j] */ for (j = 0; j < nytop; j += 2) { /* * hp is h0 (mean value) in next x zone, hm is h0 in previous x zone */ hm = a.get(s00 - ny2); h0 = a.get(s00); hp = a.get(s00 + ny2); /* * diff = 8 * hx slope that would match h0 in neighboring zones */ diff = hp - hm; /* * monotonicity constraints on diff */ dmax = Math.max(Math.min(hp - h0, h0 - hm), 0) << 2; dmin = Math.min(Math.max(hp - h0, h0 - hm), 0) << 2; /* * if monotonicity would set slope = 0 then don't change hx. note dmax>=0, dmin<=0. */ if (dmin < dmax) { diff = Math.max(Math.min(diff, dmax), dmin); /* * Compute change in slope limited to range +/- smax. Careful with rounding negative numbers when * using shift for divide by 8. */ s = diff - (a.get(s10) << N03); s = s >= 0 ? s >> N03 : s + N07 >> N03; s = Math.max(Math.min(s, smax), -smax); a.set(s10, a.get(s10) + s); } s00 += 2; s10 += 2; } } /* * Adjust y difference hy */ for (i = 0; i < nxtop; i += 2) { s00 = ny * i + 2; s10 = s00 + ny; for (j = 2; j < nytop - 2; j += 2) { hm = a.get(s00 - 2); h0 = a.get(s00); hp = a.get(s00 + 2); diff = hp - hm; dmax = Math.max(Math.min(hp - h0, h0 - hm), 0) << 2; dmin = Math.min(Math.max(hp - h0, h0 - hm), 0) << 2; if (dmin < dmax) { diff = Math.max(Math.min(diff, dmax), dmin); s = diff - (a.get(s00 + 1) << N03); s = s >= 0 ? s >> N03 : s + N07 >> N03; s = Math.max(Math.min(s, smax), -smax); a.set(s00 + 1, a.get(s00 + 1) + s); } s00 += 2; s10 += 2; } } /* * Adjust curvature difference hc */ for (i = 2; i < nxtop - 2; i += 2) { s00 = ny * i + 2; s10 = s00 + ny; for (j = 2; j < nytop - 2; j += 2) { /* * ------------------ y | hmp | | hpp | | ------------------ | | | h0 | | | ------------------ -------x * | hmm | | hpm | ------------------ */ hmm = a.get(s00 - ny2 - 2); hpm = a.get(s00 + ny2 - 2); hmp = a.get(s00 - ny2 + 2); hpp = a.get(s00 + ny2 + 2); h0 = a.get(s00); /* * diff = 64 * hc value that would match h0 in neighboring zones */ diff = hpp + hmm - hmp - hpm; /* * 2 times x,y slopes in this zone */ hx2 = a.get(s10) << 1; hy2 = a.get(s00 + 1) << 1; /* * monotonicity constraints on diff */ m1 = Math.min(Math.max(hpp - h0, 0) - hx2 - hy2, Math.max(h0 - hpm, 0) + hx2 - hy2); m2 = Math.min(Math.max(h0 - hmp, 0) - hx2 + hy2, Math.max(hmm - h0, 0) + hx2 + hy2); dmax = Math.min(m1, m2) << BITS_OF_1_NYBBLE; m1 = Math.max(Math.min(hpp - h0, 0) - hx2 - hy2, Math.min(h0 - hpm, 0) + hx2 - hy2); m2 = Math.max(Math.min(h0 - hmp, 0) - hx2 + hy2, Math.min(hmm - h0, 0) + hx2 + hy2); dmin = Math.max(m1, m2) << BITS_OF_1_NYBBLE; /* * if monotonicity would set slope = 0 then don't change hc. note dmax>=0, dmin<=0. */ if (dmin < dmax) { diff = Math.max(Math.min(diff, dmax), dmin); /* * Compute change in slope limited to range +/- smax. Careful with rounding negative numbers when * using shift for divide by 64. */ s = diff - (a.get(s10 + 1) << N06); s = s >= 0 ? s >> N06 : s + N63 >> N06; s = Math.max(Math.min(s, smax), -smax); a.set(s10 + 1, a.get(s10 + 1) + s); } s00 += 2; s10 += 2; } } } private int inputBit(ByteBuffer infile) { if (bitsToGo == 0) { /* Read the next byte if no */ buffer2 = infile.get() & BYTE_MASK; bitsToGo = BITS_OF_1_BYTE; } /* * Return the next bit */ bitsToGo--; return buffer2 >> bitsToGo & 1; } /* * Huffman decoding for fixed codes Coded values range from 0-15 Huffman code values (hex): 3e, 00, 01, 08, 02, 09, * 1a, 1b, 03, 1c, 0a, 1d, 0b, 1e, 3f, 0c and number of bits in each code: 6, 3, 3, 4, 3, 4, 5, 5, 3, 5, 4, 5, 4, 5, * 6, 4 */ private int inputHuffman(ByteBuffer infile) { int c; /* * get first 3 bits to start */ c = inputNbits(infile, N03); if (c < N04) { /* * this is all we need return 1,2,4,8 for c=0,1,2,3 */ return 1 << c; } /* * get the next bit */ c = inputBit(infile) | c << 1; if (c < N13) { /* * OK, 4 bits is enough */ switch (c) { case N08: return N03; case N09: return N05; case N10: return N10; case N11: return N12; case N12: return N15; default: } } /* * get yet another bit */ c = inputBit(infile) | c << 1; if (c < N31) { /* * OK, 5 bits is enough */ switch (c) { case N26: return N06; case N27: return N07; case N28: return N09; case N29: return N11; case N30: return N13; default: } } /* * need the 6th bit */ c = inputBit(infile) | c << 1; if (c == N62) { return 0; } return N14; } private int inputNbits(ByteBuffer infile, int n) { if (bitsToGo < n) { /* * need another byte's worth of bits */ buffer2 = buffer2 << BITS_OF_1_BYTE | infile.get() & BYTE_MASK; bitsToGo += BITS_OF_1_BYTE; } /* * now pick off the first n bits */ bitsToGo -= n; /* there was a slight gain in speed by replacing the following line */ /* return( (buffer2>>bits_to_go) & ((1<> bitsToGo & MASKS[n]; } /* INITIALIZE BIT INPUT */ private int inputNnybble(ByteBuffer infile, int n, byte[] array) { /* * copy n 4-bit nybbles from infile to the lower 4 bits of tiledImageOperation */ int ii, kk, shift1, shift2; /* * forcing byte alignment doesn;t help, and even makes it go slightly slower if (bits_to_go != 8) * input_nbits(infile, bits_to_go); */ if (n == 1) { array[0] = (byte) inputNybble(infile); return 0; } if (bitsToGo == BITS_OF_1_BYTE) { /* * already have 2 full nybbles in buffer2, so backspace the infile tiledImageOperation to reuse last char */ infile.position(infile.position() - 1); bitsToGo = 0; } /* bits_to_go now has a value in the range 0 - 7. After adding */ /* another byte, bits_to_go effectively will be in range 8 - 15 */ shift1 = bitsToGo + BITS_OF_1_NYBBLE; /* * shift1 will be in range 4 - 11 */ shift2 = bitsToGo; /* shift2 will be in range 0 - 7 */ kk = 0; /* special case */ if (bitsToGo == 0) { for (ii = 0; ii < n / 2; ii++) { /* * refill the buffer with next byte */ buffer2 = buffer2 << BITS_OF_1_BYTE | infile.get() & BYTE_MASK; array[kk] = (byte) (buffer2 >> BITS_OF_1_NYBBLE & NYBBLE_MASK); array[kk + 1] = (byte) (buffer2 & NYBBLE_MASK); /* * no shift required */ kk += 2; } } else { for (ii = 0; ii < n / 2; ii++) { /* * refill the buffer with next byte */ buffer2 = buffer2 << BITS_OF_1_BYTE | infile.get() & BYTE_MASK; array[kk] = (byte) (buffer2 >> shift1 & NYBBLE_MASK); array[kk + 1] = (byte) (buffer2 >> shift2 & NYBBLE_MASK); kk += 2; } } if (ii * 2 != n) { /* have to read last odd byte */ array[n - 1] = (byte) inputNybble(infile); } return buffer2 >> bitsToGo & NYBBLE_MASK; } private int inputNybble(ByteBuffer infile) { if (bitsToGo < BITS_OF_1_NYBBLE) { /* * need another byte's worth of bits */ buffer2 = buffer2 << BITS_OF_1_BYTE | infile.get() & BYTE_MASK; bitsToGo += BITS_OF_1_BYTE; } /* * now pick off the first 4 bits */ bitsToGo -= BITS_OF_1_NYBBLE; return buffer2 >> bitsToGo & NYBBLE_MASK; } /** * Copy 4-bit values from a[(nx+1)/2,(ny+1)/2] to b[nx,ny], expanding each value to 2x2 pixels and inserting into * bitplane BIT of B. A,B may NOT be same tiledImageOperation (it wouldn't make sense to be inserting bits into the * same tiledImageOperation anyway.) */ private void qtreeBitins64(byte[] a, int lnx, int lny, LongArrayPointer b, int n, int bit) { int i, j, s00; long planeVal = 1L << bit; // expand each 2x2 block ByteBuffer k = ByteBuffer.wrap(a); /* k is index of a[i/2,j/2] */ for (i = 0; i < lnx - 1; i += 2) { s00 = n * i; /* s00 is index of b[i,j] */ // Note: this code appears to run very slightly faster on a 32-bit // linux machine using s00+n rather than the s10 intermediate // variable // s10 = s00+n; *//* s10 is index of b[i+1,j] for (j = 0; j < lny - 1; j += 2) { byte value = k.get(); if ((value & BIT_ONE) != ZERO) { b.bitOr(s00 + n + 1, planeVal); } if ((value & BIT_TWO) != ZERO) { b.bitOr(s00 + n, planeVal); } if ((value & BIT_THREE) != ZERO) { b.bitOr(s00 + 1, planeVal); } if ((value & BIT_FOUR) != ZERO) { b.bitOr(s00, planeVal); } // b.bitOr(s10+1, ((LONGLONG) ( a[k] & 1)) << bit; b.bitOr(s10 , // ((((LONGLONG)a[k])>>1) & 1) << bit; b.bitOr(s00+1, // ((((LONGLONG)a[k])>>2) & 1) << bit; b.bitOr(s00 // ,((((LONGLONG)a[k])>>3) & 1) << bit; s00 += 2; /* s10 += 2; */ } if (j < lny) { // row size is odd, do last element in row s00+1, s10+1 are off // edge byte value = k.get(); if ((value & BIT_TWO) != ZERO) { b.bitOr(s00 + n, planeVal); } if ((value & BIT_FOUR) != ZERO) { b.bitOr(s00, planeVal); } // b.bitOr(s10 , ((((LONGLONG)a[k])>>1) & 1) << bit; b.bitOr(s00 // , ((((LONGLONG)a[k])>>3) & 1) << bit; } } if (i < lnx) { // column size is odd, do last row s10, s10+1 are off edge s00 = n * i; for (j = 0; j < lny - 1; j += 2) { byte value = k.get(); if ((value & BIT_THREE) != ZERO) { b.bitOr(s00 + 1, planeVal); } if ((value & BIT_FOUR) != ZERO) { b.bitOr(s00, planeVal); } // b.bitOr(s00+1, ((((LONGLONG)a[k])>>2) & 1) << bit; // b.bitOr(s00 , ((((LONGLONG)a[k])>>3) & 1) << bit; s00 += 2; } if (j < lny) { // both row and column size are odd, do corner element s00+1, // s10, s10+1 are off edge if ((k.get() & BIT_FOUR) != ZERO) { b.bitOr(s00, planeVal); } // b.bitOr(s00 , ((((LONGLONG)a[k])>>3) & 1) << bit; } } } /** * copy 4-bit values from a[(nx+1)/2,(ny+1)/2] to b[nx,ny], expanding each value to 2x2 pixels a,b may be same * tiledImageOperation */ private void qtreeCopy(byte[] a, int lnx, int lny, byte[] b, int n) { int i, j, k, nx2, ny2; int s00, s10; // first copy 4-bit values to b start at end in case a,b are same // tiledImageOperation nx2 = (lnx + 1) / 2; ny2 = (lny + 1) / 2; k = ny2 * (nx2 - 1) + ny2 - 1; /* k is index of a[i,j] */ for (i = nx2 - 1; i >= 0; i--) { s00 = 2 * (n * i + ny2 - 1); /* s00 is index of b[2*i,2*j] */ for (j = ny2 - 1; j >= 0; j--) { b[s00] = a[k]; k--; s00 -= 2; } } for (i = 0; i < lnx - 1; i += 2) { // now expand each 2x2 block // Note: Unlike the case in qtree_bitins, this code runs faster on a // 32-bit linux machine using the s10 intermediate variable, rather // that using s00+n. Go figure! s00 = n * i; // s00 is index of b[i,j] s10 = s00 + n; // s10 is index of b[i+1,j] for (j = 0; j < lny - 1; j += 2) { b[s10 + 1] = (b[s00] & BIT_ONE) == ZERO ? ZERO : BIT_ONE; b[s10] = (b[s00] & BIT_TWO) == ZERO ? ZERO : BIT_ONE; b[s00 + 1] = (b[s00] & BIT_THREE) == ZERO ? ZERO : BIT_ONE; b[s00] = (b[s00] & BIT_FOUR) == ZERO ? ZERO : BIT_ONE; s00 += 2; s10 += 2; } if (j < lny) { // row size is odd, do last element in row s00+1, s10+1 are off // edge not worth converting this to use 16 case statements b[s10] = (byte) (b[s00] >> 1 & 1); b[s00] = (byte) (b[s00] >> N03 & 1); } } if (i < lnx) { // column size is odd, do last row s10, s10+1 are off edge s00 = n * i; for (j = 0; j < lny - 1; j += 2) { // not worth converting this to use 16 case statements b[s00 + 1] = (byte) (b[s00] >> 2 & 1); b[s00] = (byte) (b[s00] >> N03 & 1); s00 += 2; } if (j < lny) { // both row and column size are odd, do corner element s00+1, // s10, s10+1 are off edge not worth converting this to use 16 // case statements b[s00] = (byte) (b[s00] >> N03 & 1); } } } /** * char *infile; long a[]; a is 2-D tiledImageOperation with dimensions (n,n) int n; length of full row in a int * nqx; partial length of row to decode int nqy; partial length of column (<=n) int nbitplanes; number of bitplanes * to decode */ private int qtreeDecode64(ByteBuffer infile, LongArrayPointer a, int n, int nqx, int nqy, int nbitplanes) { int k, bit, b; int nx2, ny2, nfx, nfy, c; byte[] scratch; /* * log2n is log2 of max(nqx,nqy) rounded up to next power of 2 */ int nqmax = nqx > nqy ? nqx : nqy; int log2n = calculateLog2N(nqmax); /* * allocate scratch tiledImageOperation for working space */ int nqx2 = (nqx + 1) / 2; int nqy2 = (nqy + 1) / 2; scratch = new byte[nqx2 * nqy2]; /* * now decode each bit plane, starting at the top A is assumed to be initialized to zero */ for (bit = nbitplanes - 1; bit >= 0; bit--) { /* * Was bitplane was quadtree-coded or written directly? */ b = inputNybble(infile); if (b == 0) { /* * bit map was written directly */ readBdirect64(infile, a, n, nqx, nqy, scratch, bit); } else if (b != NYBBLE_MASK) { throw new RuntimeException("Compression error"); } else { /* * bitmap was quadtree-coded, do log2n expansions read first code */ scratch[0] = (byte) inputHuffman(infile); /* * now do log2n expansions, reading codes from file as necessary */ nx2 = 1; ny2 = 1; nfx = nqx; nfy = nqy; c = 1 << log2n; for (k = 1; k < log2n; k++) { /* * this somewhat cryptic code generates the sequence n[k-1] = (n[k]+1)/2 where n[log2n]=nqx or nqy */ c = c >> 1; nx2 = nx2 << 1; ny2 = ny2 << 1; if (nfx <= c) { nx2--; } else { nfx -= c; } if (nfy <= c) { ny2--; } else { nfy -= c; } qtreeExpand(infile, scratch, nx2, ny2, scratch); } /* * now copy last set of 4-bit codes to bitplane bit of tiledImageOperation a */ qtreeBitins64(scratch, nqx, nqy, a, n, bit); } } return 0; } /* * do one quadtree expansion step on tiledImageOperation a[(nqx+1)/2,(nqy+1)/2] results put into b[nqx,nqy] (which * may be the same as a) */ private void qtreeExpand(ByteBuffer infile, byte[] a, int nx2, int ny2, byte[] b) { int i; /* * first copy a to b, expanding each 4-bit value */ qtreeCopy(a, nx2, ny2, b, ny2); /* * now read new 4-bit values into b for each non-zero element */ for (i = nx2 * ny2 - 1; i >= 0; i--) { if (b[i] != 0) { b[i] = (byte) inputHuffman(infile); } } } private void readBdirect64(ByteBuffer infile, LongArrayPointer a, int n, int nqx, int nqy, byte[] scratch, int bit) { /* * read bit image packed 4 pixels/nybble */ /* * int i; for (i = 0; i < ((nqx+1)/2) * ((nqy+1)/2); i++) { scratch[i] = input_nybble(infile); } */ inputNnybble(infile, (nqx + 1) / 2 * ((nqy + 1) / 2), scratch); /* * insert in bitplane BIT of image A */ qtreeBitins64(scratch, nqx, nqy, a, n, bit); } /* * ########################################################################## ## */ private void startInputingBits() { /* * Buffer starts out with no bits in it */ bitsToGo = 0; } private void undigitize64(LongArrayPointer a) { long scale64; /* * multiply by scale */ if (scale <= 1) { return; } scale64 = scale; /* * use a 64-bit int for efficiency in the big loop */ for (int index = 0; index < a.a.length; index++) { a.a[index] = a.a[index] * scale64; } } /** * long a[]; tiledImageOperation to shuffle int n; number of elements to shuffle int n2; second dimension long * tmp[]; scratch storage */ private void unshuffle64(LongArrayPointer a, int n, int n2, long[] tmp) { int i; int nhalf; LongArrayPointer p1, p2, pt; /* * copy 2nd half of tiledImageOperation to tmp */ nhalf = n + 1 >> 1; pt = new LongArrayPointer(tmp); p1 = a.copy(n2 * nhalf); /* pointer to a[i] */ for (i = nhalf; i < n; i++) { pt.set(p1.get()); p1.offset += n2; pt.offset++; } /* * distribute 1st half of tiledImageOperation to even elements */ p2 = a.copy(n2 * (nhalf - 1)); /* pointer to a[i] */ p1 = a.copy(n2 * (nhalf - 1) << 1); /* pointer to a[2*i] */ for (i = nhalf - 1; i >= 0; i--) { p1.set(p2.get()); p2.offset -= n2; p1.offset -= n2 + n2; } /* * now distribute 2nd half of tiledImageOperation (in tmp) to odd elements */ pt = new LongArrayPointer(tmp); p1 = a.copy(n2); /* pointer to a[i] */ for (i = 1; i < n; i += 2) { p1.set(pt.get()); p1.offset += n2 + n2; pt.offset++; } } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/hcompress/package-info.java000066400000000000000000000042151476377620500326250ustar00rootroot00000000000000/** *

* The HCompress algorithm and its options. HCompress is a * lossless compression algorithms was developed by Richard L. White at the Space Telescope Science Institute, and is * used for the Hubble archive and the STScI Digital Sky Survey. It's weird that it's a package by itself, but that's * life. *

*

* The only classes in here that users would typically interact with are * {@link nom.tam.fits.compression.algorithm.hcompress.HCompressorOption} and potentially * {@link nom.tam.fits.compression.algorithm.hcompress.HCompressorQuantizeOption}, e.g. via * {@link nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class)} to set options after HCompress was * selected for compressing an image HDU. *

*/ package nom.tam.fits.compression.algorithm.hcompress; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/plio/000077500000000000000000000000001476377620500263745ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/plio/PLIOCompress.java000066400000000000000000000303431476377620500315210ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.plio; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; import nom.tam.fits.compression.algorithm.api.ICompressor; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * (for internal use) The PLIO compression algorithm. The original decompression code was written by Doug Tody, * NRAO and included (ported to c and adapted) in cfitsio by William Pence, NASA/GSFC. That code was then ported to Java * by R. van Nieuwenhoven. Later it was massively refactored to harmonize the different compression algorithms and * reduce the duplicate code pieces without obscuring the algorithm itself as good as possible. * * @author Doug Tody * @author William Pence * @author Richard van Nieuwenhoven */ @SuppressWarnings("javadoc") public abstract class PLIOCompress { public static class BytePLIOCompressor extends PLIOCompress implements ICompressor { private ByteBuffer pixelData; @Override public boolean compress(ByteBuffer buffer, ByteBuffer compressed) { pixelData = buffer; compress(compressed.asShortBuffer(), pixelData.limit()); return true; } @Override public void decompress(ByteBuffer compressed, ByteBuffer buffer) { pixelData = buffer; decompress(compressed.asShortBuffer(), pixelData.limit()); } @Override protected int nextPixel() { return pixelData.get(); } @Override protected void put(int index, int pixel) { pixelData.put(index, (byte) pixel); } } public static class ShortPLIOCompressor extends PLIOCompress implements ICompressor { private ShortBuffer pixelData; @Override public boolean compress(ShortBuffer buffer, ByteBuffer compressed) { pixelData = buffer; super.compress(compressed.asShortBuffer(), pixelData.limit()); return true; } @Override public void decompress(ByteBuffer compressed, ShortBuffer buffer) { pixelData = buffer; decompress(compressed.asShortBuffer(), pixelData.limit()); } @Override protected int nextPixel() { return pixelData.get(); } @Override protected void put(int index, int pixel) { pixelData.put(index, (short) pixel); } } /** * Attention int values are limited to 24 bits! */ public static class IntPLIOCompressor extends PLIOCompress implements ICompressor { private IntBuffer pixelData; @Override public boolean compress(IntBuffer buffer, ByteBuffer compressed) { pixelData = buffer; super.compress(compressed.asShortBuffer(), pixelData.limit()); return true; } @Override public void decompress(ByteBuffer compressed, IntBuffer buffer) { pixelData = buffer; decompress(compressed.asShortBuffer(), pixelData.limit()); } @Override protected int nextPixel() { return pixelData.get(); } @Override protected void put(int index, int pixel) { pixelData.put(index, (short) pixel); } } private static final int FIRST_VALUE_WITH_13_BIT = 4096; private static final int FIRST_VALUE_WITH_14_BIT = 8192; private static final int FIRST_VALUE_WITH_15_BIT = 16384; private static final int FIRST_VALUE_WITH_16_BIT = 32768; private static final int HEADER_SIZE_FIELD1 = 3; private static final int HEADER_SIZE_FIELD2 = 4; private static final int LAST_VALUE_FITTING_IN_12_BIT = FIRST_VALUE_WITH_13_BIT - 1; private static final int MINI_HEADER_SIZE = 3; private static final int MINI_HEADER_SIZE_FIELD = 2; /** * The exact meaning of this var is not clear at the moment of porting the algorithm to Java. */ private static final int N20481 = 20481; private static final int OPCODE_1 = 1; private static final int OPCODE_2 = 2; private static final int OPCODE_3 = 3; private static final int OPCODE_4 = 4; private static final int OPCODE_5 = 5; private static final int OPCODE_6 = 6; private static final int OPCODE_7 = 7; private static final int OPCODE_8 = 8; private static final short[] PLIO_HEADER = {(short) 0, (short) 7, (short) -100, (short) 0, (short) 0, (short) 0, (short) 0}; private static final int SHIFT_12_BITS = 12; private static final int SHIFT_15_BITS = 15; private static final int VALUE_OF_BIT_13_AND14_ON = 12288; /** * PL_P2L -- Convert a pixel tiledImageOperation to a line list. The length of the list is returned as the function * value. * * @param compressedData encoded line list * @param npix number of pixels to convert */ protected void compress(ShortBuffer compressedData, int npix) { compressedData.put(PLIO_HEADER); final int xe = npix - 1; int op = PLIO_HEADER.length; /* Computing MAX */ int pv = Math.max(0, nextPixel()); int x1 = 0; int iz = 0; int hi = 1; int nv = 0; for (int ip = 0; ip <= xe; ++ip) { if (ip < xe) { /* Computing MAX */ nv = Math.max(0, nextPixel()); if (nv == pv) { continue; } if (pv == 0) { pv = nv; x1 = ip + 1; continue; } } else if (pv == 0) { x1 = xe + 1; } int np = ip - x1 + 1; int nz = x1 - iz; boolean skip = false; if (pv > 0) { int dv = pv - hi; if (dv != 0) { hi = pv; if (Math.abs(dv) > LAST_VALUE_FITTING_IN_12_BIT) { compressedData.put(op, (short) ((pv & LAST_VALUE_FITTING_IN_12_BIT) + FIRST_VALUE_WITH_13_BIT)); ++op; compressedData.put(op, (short) (pv / FIRST_VALUE_WITH_13_BIT)); ++op; } else { if (dv < 0) { compressedData.put(op, (short) (-dv + VALUE_OF_BIT_13_AND14_ON)); } else { compressedData.put(op, (short) (dv + FIRST_VALUE_WITH_14_BIT)); } ++op; if (np == 1 && nz == 0) { int v = compressedData.get(op - 1); compressedData.put(op - 1, (short) (v | FIRST_VALUE_WITH_15_BIT)); skip = true; } } } } if (!skip) { if (nz > 0) { while (nz > 0) { compressedData.put(op, (short) Math.min(LAST_VALUE_FITTING_IN_12_BIT, nz)); ++op; nz += -LAST_VALUE_FITTING_IN_12_BIT; } if (np == 1 && pv > 0) { compressedData.put(op - 1, (short) (compressedData.get(op - 1) + N20481)); skip = true; } } } if (!skip) { while (np > 0) { compressedData.put(op, (short) (Math.min(LAST_VALUE_FITTING_IN_12_BIT, np) + FIRST_VALUE_WITH_15_BIT)); ++op; np += -LAST_VALUE_FITTING_IN_12_BIT; } } x1 = ip + 1; iz = x1; pv = nv; } compressedData.put(HEADER_SIZE_FIELD1, (short) (op % FIRST_VALUE_WITH_16_BIT)); compressedData.put(HEADER_SIZE_FIELD2, (short) (op / FIRST_VALUE_WITH_16_BIT)); compressedData.position(op); } /** * PL_L2PI -- Translate a PLIO line list into an integer pixel tiledImageOperation. The number of pixels output * (always npix) is returned as the function value. * * @param compressedData encoded line list * @param npix number of pixels to convert * * @return number of pixels converted */ protected int decompress(ShortBuffer compressedData, int npix) { int llfirt; int lllen; if (!(compressedData.get(2) > 0)) { lllen = (compressedData.get(HEADER_SIZE_FIELD2) << SHIFT_15_BITS) + compressedData.get(HEADER_SIZE_FIELD1); llfirt = compressedData.get(1); } else { lllen = compressedData.get(MINI_HEADER_SIZE_FIELD); llfirt = MINI_HEADER_SIZE; } final int xe = npix; int op = 0; int x1 = 1; int pv = 1; for (int ip = llfirt; ip <= lllen; ++ip) { final int opcode = compressedData.get(ip) / FIRST_VALUE_WITH_13_BIT; final int data = compressedData.get(ip) & LAST_VALUE_FITTING_IN_12_BIT; final int sw0001 = opcode + 1; if (sw0001 == OPCODE_1 || sw0001 == OPCODE_5 || sw0001 == OPCODE_6) { final int x2 = x1 + data - 1; final int i2 = Math.min(x2, xe); final int np = i2 - Math.max(x1, 0) + 1; if (np > 0) { final int otop = op + np - 1; if (!(opcode == OPCODE_4)) { for (int index = op; index <= otop; ++index) { put(index, 0); } if (opcode == OPCODE_5 && i2 == x2) { put(otop, pv); } } else { for (int index = op; index <= otop; ++index) { put(index, pv); } } op = otop + 1; } x1 = x2 + 1; } else if (sw0001 == OPCODE_2) { pv = (compressedData.get(ip + 1) << SHIFT_12_BITS) + data; ++ip; } else if (sw0001 == OPCODE_3) { pv += data; } else if (sw0001 == OPCODE_4) { pv -= data; } else if (sw0001 == OPCODE_7) { pv += data; if (x1 >= 0 && x1 <= xe) { put(op, pv); ++op; } ++x1; } else if (sw0001 == OPCODE_8) { pv -= data; if (x1 >= 0 && x1 <= xe) { put(op, pv); ++op; } ++x1; } if (x1 > xe) { break; } } for (int index = op; index < npix; ++index) { put(index, 0); } return npix; } protected abstract int nextPixel(); protected abstract void put(int index, int pixel); } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/plio/package-info.java000066400000000000000000000035601476377620500315670ustar00rootroot00000000000000/** *

* (for internal use) PLIO compression algorithm. The original decompression code was written by Doug Tody, NRAO * and included (ported to C and adapted) in cfitsio by William Pence, NASA/GSFC. That code was then ported to * Java by R. van Nieuwenhoven. Later it was massively refactored to harmonize the different compression algorithms and * reduce the duplicate code pieces without obscuring the algorithm itself as good as possible. It's weird that it's a * package by itself, but that's life. *

*/ package nom.tam.fits.compression.algorithm.plio; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/quant/000077500000000000000000000000001476377620500265615ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/quant/Quantize.java000066400000000000000000000455651476377620500312430ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.quant; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.util.Arrays; /** * (for internal use) Determines the optimal quantization to use for floating-point data. It estimates the noise * level in the data to determine qhat quantization should be use to lose no information above the noise level. * * @deprecated (for internal use) This class sohuld have visibility reduced to the package level */ @SuppressWarnings("javadoc") public class Quantize { class DoubleArrayPointer { private final double[] array; private int startIndex; DoubleArrayPointer(double[] arrayIn) { array = arrayIn; } public DoubleArrayPointer copy(long l) { DoubleArrayPointer result = new DoubleArrayPointer(array); result.startIndex = (int) l; return result; } public double get(int ii) { return array[ii + startIndex]; } } private static final double DEFAULT_QUANT_LEVEL = 4.; private static final double MAX_INT_AS_DOUBLE = Integer.MAX_VALUE; private static final int MINIMUM_PIXEL_WIDTH = 9; /** * number of reserved values, starting with */ private static final long N_RESERVED_VALUES = 10; private static final int N4 = 4; private static final int N6 = 6; private static final double NOISE_2_MULTIPLICATOR = 1.0483579; private static final double NOISE_3_MULTIPLICATOR = 0.6052697; private static final double NOISE_5_MULTIPLICATOR = 0.1772048; private final QuantizeOption parameter; /** * maximum non-null value */ private double maxValue; /** * minimum non-null value */ private double minValue; /** * number of good, non-null pixels? */ private long ngood; /** * returned 2nd order MAD of all non-null pixels */ private double noise2; /** * returned 3rd order MAD of all non-null pixels */ private double noise3; /* returned 5th order MAD of all non-null pixels */ private double noise5; private double xmaxval; private double xminval; private double xnoise2; private double xnoise3; private double xnoise5; public Quantize(QuantizeOption quantizeOption) { parameter = quantizeOption; } /** * Estimate the median and background noise in the input image using 2nd, 3rd and 5th order Median Absolute * Differences. The noise in the background of the image is calculated using the MAD algorithms developed for * deriving the signal to noise ratio in spectra (see issue #42 of the ST-ECF newsletter, * http://www.stecf.org/documents/newsletter/) 3rd order: noise = 1.482602 / sqrt(6) * median (abs(2*flux(i) - * flux(i-2) - flux(i+2))) The returned estimates are the median of the values that are computed for each row of the * image. * * @param arrayIn 2 dimensional tiledImageOperation of image pixels * @param nx number of pixels in each row of the image * @param ny number of rows in the image * @param nullcheck check for null values, if true * @param nullvalue value of null pixels, if nullcheck is true */ private void calculateNoise(double[] arrayIn, int nx, int ny) { DoubleArrayPointer array = new DoubleArrayPointer(arrayIn); initializeNoise(); if (nx < MINIMUM_PIXEL_WIDTH) { // treat entire tiledImageOperation as an image with a single row nx = nx * ny; ny = 1; } if (calculateNoiseShortRow(array, nx, ny)) { return; } DoubleArrayPointer rowpix; int nrows = 0, nrows2 = 0; long ngoodpix = 0; /* allocate arrays used to compute the median and noise estimates */ double[] differences2 = new double[nx]; double[] differences3 = new double[nx]; double[] differences5 = new double[nx]; double[] diffs2 = new double[ny]; double[] diffs3 = new double[ny]; double[] diffs5 = new double[ny]; /* loop over each row of the image */ for (int jj = 0; jj < ny; jj++) { rowpix = array.copy(jj * nx); /* point to first pixel in the row */ int ii = 0; ii = findNextValidPixelWithNullCheck(nx, rowpix, ii); if (ii == nx) { continue; /* hit end of row */ } double v1 = getNextPixelAndCheckMinMax(rowpix, ii); ngoodpix++; ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii); if (ii == nx) { continue; /* hit end of row */ } double v2 = getNextPixelAndCheckMinMax(rowpix, ii); ngoodpix++; ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii); if (ii == nx) { continue; /* hit end of row */ } double v3 = getNextPixelAndCheckMinMax(rowpix, ii); ngoodpix++; ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii); if (ii == nx) { continue; /* hit end of row */ } double v4 = getNextPixelAndCheckMinMax(rowpix, ii); ngoodpix++; ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii); if (ii == nx) { continue; /* hit end of row */ } double v5 = getNextPixelAndCheckMinMax(rowpix, ii); ngoodpix++; ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii); if (ii == nx) { continue; /* hit end of row */ } double v6 = getNextPixelAndCheckMinMax(rowpix, ii); ngoodpix++; ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii); if (ii == nx) { continue; /* hit end of row */ } double v7 = getNextPixelAndCheckMinMax(rowpix, ii); ngoodpix++; ii = findNextValidPixelWithNullCheck(nx, rowpix, ++ii); if (ii == nx) { continue; /* hit end of row */ } double v8 = getNextPixelAndCheckMinMax(rowpix, ii); ngoodpix++; // now populate the differences arrays for the remaining pixels in // the row */ int nvals = 0; int nvals2 = 0; for (ii++; ii < nx; ii++) { ii = findNextValidPixelWithNullCheck(nx, rowpix, ii); if (ii == nx) { continue; /* hit end of row */ } double v9 = getNextPixelAndCheckMinMax(rowpix, ii); /* construct tiledImageOperation of absolute differences */ if (!(v5 == v6 && v6 == v7)) { differences2[nvals2] = Math.abs(v5 - v7); nvals2++; } if (!(v3 == v4 && v4 == v5 && v5 == v6 && v6 == v7)) { differences3[nvals] = Math.abs(2 * v5 - v3 - v7); differences5[nvals] = Math.abs(N6 * v5 - N4 * v3 - N4 * v7 + v1 + v9); nvals++; } else { /* ignore constant background regions */ ngoodpix++; } /* shift over 1 pixel */ v1 = v2; v2 = v3; v3 = v4; v4 = v5; v5 = v6; v6 = v7; v7 = v8; v8 = v9; } /* end of loop over pixels in the row */ // compute the median diffs Note that there are 8 more pixel values // than there are diffs values. ngoodpix += nvals; if (nvals == 0) { continue; /* cannot compute medians on this row */ } if (nvals == 1) { if (nvals2 == 1) { diffs2[nrows2] = differences2[0]; nrows2++; } diffs3[nrows] = differences3[0]; diffs5[nrows] = differences5[0]; } else { /* quick_select returns the median MUCH faster than using qsort */ if (nvals2 > 1) { diffs2[nrows2] = quickSelect(differences2, nvals); nrows2++; } diffs3[nrows] = quickSelect(differences3, nvals); diffs5[nrows] = quickSelect(differences5, nvals); } nrows++; } /* end of loop over rows */ computeMedianOfValuesEachRow(nrows, nrows2, diffs2, diffs3, diffs5); setNoiseResult(ngoodpix); } private boolean calculateNoiseShortRow(DoubleArrayPointer array, int nx, int ny) { /* rows must have at least 9 pixels */ if (nx < MINIMUM_PIXEL_WIDTH) { int ngoodpix = 0; for (int index = 0; index < nx; index++) { if (isNull(array.get(index))) { continue; } if (array.get(index) < xminval) { xminval = array.get(index); } if (array.get(index) > xmaxval) { xmaxval = array.get(index); } ngoodpix++; } setNoiseResult(ngoodpix); return true; } return false; } protected void computeMedianOfValuesEachRow(int nrows, int nrows2, double[] diffs2, double[] diffs3, double[] diffs5) { // compute median of the values for each row. if (nrows == 0) { xnoise3 = 0; xnoise5 = 0; } else if (nrows == 1) { xnoise3 = diffs3[0]; xnoise5 = diffs5[0]; } else { Arrays.sort(diffs3, 0, nrows); Arrays.sort(diffs5, 0, nrows); xnoise3 = (diffs3[(nrows - 1) / 2] + diffs3[nrows / 2]) / 2.; xnoise5 = (diffs5[(nrows - 1) / 2] + diffs5[nrows / 2]) / 2.; } if (nrows2 == 0) { xnoise2 = 0; } else if (nrows2 == 1) { xnoise2 = diffs2[0]; } else { Arrays.sort(diffs2, 0, nrows2); xnoise2 = (diffs2[(nrows2 - 1) / 2] + diffs2[nrows2 / 2]) / 2.; } } protected int findNextValidPixelWithNullCheck(int nx, DoubleArrayPointer rowpix, int ii) { return ii; } private double getNextPixelAndCheckMinMax(DoubleArrayPointer rowpix, int ii) { double pixelValue = rowpix.get(ii); /* store the good pixel value */ if (pixelValue < xminval) { xminval = pixelValue; } if (pixelValue > xmaxval) { xmaxval = pixelValue; } return pixelValue; } protected double getNoise2() { return noise2; } protected double getNoise3() { return noise3; } protected double getNoise5() { return noise5; } private void initializeNoise() { xnoise2 = 0; xnoise3 = 0; xnoise5 = 0; xminval = Double.MAX_VALUE; xmaxval = Double.MIN_VALUE; } protected boolean isNull(double d) { return false; } /** * arguments: long row i: tile number = row number in the binary table double fdata[] i: tiledImageOperation of * image pixels to be compressed long nxpix i: number of pixels in each row of fdata long nypix i: number of rows in * fdata nullcheck i: check for nullvalues in fdata? double in_null_value i: value used to represent undefined * pixels in fdata float qlevel i: quantization level int dither_method i; which dithering method to use int idata[] * o: values of fdata after applying bzero and bscale double bscale o: scale factor double bzero o: zero offset int * iminval o: minimum quantized value that is returned int imaxval o: maximum quantized value that is returned The * function value will be one if the input fdata were copied to idata; in this case the parameters bscale and bzero * can be used to convert back to nearly the original floating point values: fdata ~= idata * bscale + bzero. If the * function value is zero, the data were not copied to idata. *

* In earlier implementations of the compression code, we only used the noise3 value as the most reliable estimate * of the background noise in an image. If it is not possible to compute a noise3 value, then this serves as a red * flag to indicate that quantizing the image could cause a loss of significant information in the image. *

*

* At some later date, we decided to take the more conservative approach of using the minimum of all three of the * noise values (while still requiring that noise3 has a defined value) as the best estimate of the noise. Note that * if an image contains pure Gaussian distributed noise, then noise2, noise3, and noise5 will have exactly the same * value (within statistical measurement errors). *

* * @param fdata the data to quantinize * @param nxpix the image width * @param nypix the image hight * * @return true if the quantification was possible */ public boolean quantize(double[] fdata, int nxpix, int nypix) { // MAD 2nd, 3rd, and 5th order noise values double stdev; double bScale; /* bscale, 1 in intdata = delta in fdata */ long nx = (long) nxpix * (long) nypix; if (nx <= 1L) { parameter.setBScale(1.); parameter.setBZero(0.); return false; } if (parameter.getQLevel() >= 0.) { /* estimate background noise using MAD pixel differences */ calculateNoise(fdata, nxpix, nypix); // special case of an image filled with Nulls if (parameter.isCheckNull() && ngood == 0) { /* set parameters to dummy values, which are not used */ minValue = 0.; maxValue = 1.; stdev = 1; } else { // use the minimum of noise2, noise3, and noise5 as the best // noise value stdev = noise3; if (noise2 != 0. && noise2 < stdev) { stdev = noise2; } if (noise5 != 0. && noise5 < stdev) { stdev = noise5; } } if (parameter.getQLevel() == 0.) { bScale = stdev / DEFAULT_QUANT_LEVEL; /* default quantization */ } else { bScale = stdev / parameter.getQLevel(); } if (bScale == 0.) { return false; /* don't quantize */ } } else { /* negative value represents the absolute quantization level */ bScale = -parameter.getQLevel(); /* only nned to calculate the min and max values */ calculateNoise(fdata, nxpix, nypix); } /* check that the range of quantized levels is not > range of int */ if ((maxValue - minValue) / bScale > 2. * MAX_INT_AS_DOUBLE - N_RESERVED_VALUES) { return false; /* don't quantize */ } parameter.setBScale(bScale); parameter.setMinValue(minValue); parameter.setMaxValue(maxValue); parameter.setCheckNull(parameter.isCheckNull() && ngood != nx); return true; /* yes, data have been quantized */ } private double quickSelect(double[] arr, int n) { int low, high; int median; int middle, ll, hh; low = 0; high = n - 1; median = low + high >>> 1; // was (low + high) / 2; for (;;) { if (high <= low) { return arr[median]; } if (high == low + 1) { /* Two elements only */ if (arr[low] > arr[high]) { swapElements(arr, low, high); } return arr[median]; } /* Find median of low, middle and high items; swap into position low */ middle = low + high >>> 1; // was (low + high) / 2; if (arr[middle] > arr[high]) { swapElements(arr, middle, high); } if (arr[low] > arr[high]) { swapElements(arr, low, high); } if (arr[middle] > arr[low]) { swapElements(arr, middle, low); } /* Swap low item (now in position middle) into position (low+1) */ swapElements(arr, middle, low + 1); /* Nibble from each end towards middle, swapping items when stuck */ ll = low + 1; hh = high; for (;;) { do { ll++; } while (arr[low] > arr[ll]); do { hh--; } while (arr[hh] > arr[low]); if (hh < ll) { break; } swapElements(arr, ll, hh); } /* Swap middle item (in position low) back into correct position */ swapElements(arr, low, hh); /* Re-set active partition */ if (hh <= median) { low = ll; } if (hh >= median) { high = hh - 1; } } } private void setNoiseResult(long ngoodpix) { minValue = xminval; maxValue = xmaxval; ngood = ngoodpix; noise2 = NOISE_2_MULTIPLICATOR * xnoise2; noise3 = NOISE_3_MULTIPLICATOR * xnoise3; noise5 = NOISE_5_MULTIPLICATOR * xnoise5; } private void swapElements(double[] array, int one, int second) { double value = array[one]; array[one] = array[second]; array[second] = value; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/quant/QuantizeOption.java000066400000000000000000000570411476377620500324240ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.quant; import nom.tam.fits.compression.algorithm.api.ICompressOption; import nom.tam.fits.compression.provider.param.api.ICompressParameters; import nom.tam.fits.compression.provider.param.base.BundledParameters; import nom.tam.fits.compression.provider.param.quant.QuantizeParameters; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * Quantization options when they are part of the compression scheme. When compressing tables and images includes * quantization (integer representation of floating point data), users can control how exactly the quantization should * be performed. When reading compressed FITS files, these options will be set automatically based on the header values * recorded in the compressed HDU. * * @see nom.tam.image.compression.hdu.CompressedImageHDU#setQuantAlgorithm(String) * @see nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class) */ public class QuantizeOption implements ICompressOption { /** * and including NULL_VALUE. These values may not be used to represent the quantized and scaled floating point pixel * values If lossy Hcompression is used, and the tiledImageOperation contains null values, then it is also possible * for the compressed values to slightly exceed the range of the actual (lossless) values so we must reserve a * little more space value used to represent undefined pixels */ private static final int NULL_VALUE = Integer.MIN_VALUE + 1; /** Shared configuration across copies */ private Config config; /** The parameters that represent settings for this option in the FITS headers and/or compressed data columns */ protected QuantizeParameters parameters; private ICompressOption compressOption; private double bScale = Double.NaN; private double bZero = Double.NaN; private double nullValue = Double.NaN; private Integer nullValueIndicator; private boolean checkNull; private int intMaxValue; private int intMinValue; private double maxValue; private double minValue; private int tileIndex = 0; private int tileHeight; private int tileWidth; QuantizeOption() { this(null); } /** * Creates a new set of quantization options, to be used together with the specified compression options. * * @param compressOption Compression-specific options to pair with these quantization options, or null. * * @since 1.18 */ public QuantizeOption(ICompressOption compressOption) { parameters = new QuantizeParameters(this); config = new Config(); this.compressOption = compressOption; } @Override public QuantizeOption copy() { try { QuantizeOption copy = (QuantizeOption) clone(); if (compressOption != null) { copy.compressOption = compressOption.copy(); } copy.parameters = parameters.copy(copy); return copy; } catch (CloneNotSupportedException e) { throw new IllegalStateException("option could not be cloned", e); } } /** * Returns the integer value that represents missing (null) data in the quantized representation. * * @return the integer blanking value (null value). * * @see #setBNull(Integer) */ public Integer getBNull() { return nullValueIndicator; } /** * Returns the quantization level. * * @return the floating-point difference between integer levels in the quantized data. * * @see #setBScale(double) * @see #getBZero() */ public double getBScale() { return bScale; } /** * Returns the quantization offset. * * @return the floating-point value corresponding to the integer level 0. * * @see #setBZero(double) * @see #getBScale() */ public double getBZero() { return bZero; } @Override public ICompressParameters getCompressionParameters() { if (compressOption == null) { return parameters; } return new BundledParameters(parameters, compressOption.getCompressionParameters()); } /** * Returns the compression or quantization options, recast for the selected option class. * * @param the generic type of the compression option * @param clazz the option class for the compression algorithm used with the quantization, or * QunatizeOption.class for our own options. * * @return the recast options for the requested class or null id we do not have access to options * of the requested class. * * @see #getCompressOption() */ public T getCompressOption(Class clazz) { return unwrap(clazz); } /** * Returns the options for the compression algorithm that accompanies quantization. * * @return the options for the compression algorithm, or null * * @see #getCompressOption(Class) */ public final ICompressOption getCompressOption() { return compressOption; } /** * Returns the maximum integer level in the quantized representation. * * @return the maximum integer level in the quantized data. * * @see #getMaxValue() * @see #getIntMinValue() */ public int getIntMaxValue() { return intMaxValue; } /** * Returns the maximum integer level in the quantized representation. * * @return the maximum integer level in the quantized data. * * @see #getMinValue() * @see #getIntMinValue() */ public int getIntMinValue() { return intMinValue; } /** * Returns the maximum floating-point value in the data * * @return the maximum floating-point value in the data before quantization. * * @see #getIntMaxValue() * @see #getMinValue() */ public double getMaxValue() { return maxValue; } /** * Returns the minimum floating-point value in the data * * @return the minimum floating-point value in the data before quantization. * * @see #getIntMinValue() * @see #getMaxValue() */ public double getMinValue() { return minValue; } /** * Returns the floating-point value that indicates a null datum in the image before quantization is * applied. Normally, the FITS standard is that NaN values indicate null values in floating-point * images. While this class allows using other values also, they are not recommended since they are not supported by * FITS in a standard way. * * @return the floating-point value that represents a null value (missing data) in the image before * quantization. * * @see #setNullValue(double) * @see #getNullValueIndicator() * @see #isCheckNull() */ public double getNullValue() { return nullValue; } /** * @deprecated use {@link #getBNull()} instead (duplicate method). Returns the integer value that represents missing * data (null) in the quantized representation. * * @return the integer blanking value (null value). * * @see #setBNull(Integer) */ public final Integer getNullValueIndicator() { return getBNull(); } /** * Returns the quantization resolution level used for automatic qunatization. For Gaussian noise the quantization * level is the standard deviation of the noise divided by this Q value. Thus Q values of a few will ensure that * quantization retains just about all of the information in the noisy data. * * @return The current Q value, defined as the number of quantized levels per standard deviation (for Gaussian * noise). * * @see #setQlevel(double) * @see #getBScale() */ public double getQLevel() { return config.qlevel; } /** * Gets the random seed value used for dithering * * @return the random seed value used for dithering * * @see #setSeed(long) * @see RandomSequence */ public long getSeed() { return config.seed; } /** * Returns the sequential tile index that this option is currently configured for. * * @return the sequential tile index that the quantization is configured for * * @see #setTileIndex(int) */ public long getTileIndex() { return tileIndex; } /** * Returns the tile height * * @return the tile height in pixels * * @see #setTileHeight(int) * @see #getTileWidth() */ @Override public int getTileHeight() { return tileHeight; } /** * Returns the tile width * * @return the tile width in pixels * * @see #setTileWidth(int) * @see #getTileHeight() */ @Override public int getTileWidth() { return tileWidth; } /** * Checks whether we force the integer quantized level 0 to correspond to a floating-point level 0.0, when using * automatic quantization. * * @return true if we want to keep `BZERO` at 0 when quantizing automatically. * * @see #setCenterOnZero(boolean) */ public boolean isCenterOnZero() { return config.centerOnZero; } /** * Whether the floating-point data may contain null values (normally NaNs). * * @return true if we should expect null in the floating-point data. This is automatically * true if {@link #setBNull(Integer)} was called with a non-null value. * * @see #setBNull(Integer) */ public boolean isCheckNull() { return checkNull; } /** * Whether automatic quantization treats 0.0 as a special value. Normally values within the `BSCALE` quantization * level around 0.0 will be assigned the same integer quanta, and will become indistinguishable in the quantized * data. Some software may, in their misguided ways, assign exact zero values a special meaning (such as no data) in * which case we may want to distinguish these as we apply quantization. However, it is generally not a good idea to * use 0 as a special value. * * @return true to treat 0.0 (exact) as a special value, or false to treat is as any other * measured value (recommended). * * @see #setCheckZero(boolean) * @see #getBScale() */ public boolean isCheckZero() { return config.checkZero; } /** * Whether dithering is enabled * * @return true if dithering is enabled, or else false * * @see #setDither(boolean) * @see #isDither2() */ public boolean isDither() { return config.dither; } /** * Whether dither method 2 is used. * * @return true if dither method 2 is used, or else false * * @see #setDither2(boolean) * @see #isDither() */ public boolean isDither2() { return config.dither2; } @Override public boolean isLossyCompression() { return true; } /** * Sets the integer value that represents missing data (null) in the quantized representation. * * @param blank the new integer blanking value (that is one that denotes a missing or null datum). * Setting this option to null disables the treatment of issing or null * data. * * @return itself * * @see #getBNull() * @see #isCheckNull() */ public ICompressOption setBNull(Integer blank) { if (blank != null) { nullValueIndicator = blank; checkNull = true; } else { checkNull = false; } return this; } /** * Sets the quantization level. * * @param value the new floating-point difference between integer levels in the quantized data. * * @return itself * * @see #setQlevel(double) * @see #setBZero(double) * @see #getBScale() */ public QuantizeOption setBScale(double value) { bScale = value; return this; } /** * Sets the quantization offset. * * @param value the new floating-point value corresponding to the integer level 0. * * @return itself * * @see #setBScale(double) * @see #getBZero() */ public QuantizeOption setBZero(double value) { bZero = value; return this; } /** * Enabled or disables keeping `BZERO` at 0 when using automatic quantization. * * @param value true to keep `BZERO` at 0 when quantizing automatically, that is keep the integer * quantized level 0 correspond to floating-point level 0.0. Or, false to let the * automatic quantization algorithm determine the optimal quantization offset. * * @return iftself * * @see #isCenterOnZero() */ public QuantizeOption setCenterOnZero(boolean value) { config.centerOnZero = value; return this; } /** * @deprecated {@link #setBNull(Integer)} controls this feature automatically as needed. Sets whether we * should expect the floating-point data to contain null values (normally NaNs). * * @param value true if the floating-point data may contain null values. * * @return itself * * @see #setCheckNull(boolean) * @see #setBNull(Integer) * @see #getNullValue() */ public QuantizeOption setCheckNull(boolean value) { checkNull = value; if (nullValueIndicator == null) { nullValueIndicator = NULL_VALUE; } return this; } /** * Sets whether automatic quantization is to treat 0.0 as a special value. Normally values within the `BSCALE` * quantization level around 0.0 will be assigned the same integer quanta, and will become indistinguishable in the * quantized data. However some software may assign exact zero values a special meaning (such as no data) in which * case we may want to distinguish these as we apply qunatization. However, it is generally not a good idea to use 0 * as a special value. To mark missing data, the FITS standard recognises only NaN as a special value -- while all * other values should constitute valid measurements. * * @deprecated It is strongly discouraged to treat 0.0 values as special. FITS only recognises NaN as a * special floating-point value marking missing data. All other floating point values are * considered valid measurements. * * @param value whether to treat values around 0.0 as special. * * @return itself * * @see #isCheckZero() */ public QuantizeOption setCheckZero(boolean value) { config.checkZero = value; return this; } /** * Enables or disables dithering. * * @param value true to enable dithering, or else false to disable * * @return itself * * @see #isDither() * @see #setDither2(boolean) */ public QuantizeOption setDither(boolean value) { config.dither = value; return this; } /** * Sets whether dithering is to use method 2. * * @param value true to use dither method 2, or else false for method 1. * * @return itself * * @see #isDither2() * @see #setDither(boolean) */ public QuantizeOption setDither2(boolean value) { config.dither2 = value; return this; } /** * Sets the maximum integer level in the quantized representation. * * @param value the new maximum integer level in the quantized data. * * @return itself * * @see #getIntMaxValue() * @see #setIntMinValue(int) */ public QuantizeOption setIntMaxValue(int value) { intMaxValue = value; return this; } /** * Sets the minimum integer level in the quantized representation. * * @param value the new minimum integer level in the quantized data. * * @return itself * * @see #getIntMinValue() * @see #setIntMaxValue(int) */ public QuantizeOption setIntMinValue(int value) { intMinValue = value; return this; } /** * Sets the maximum floating-point value in the data * * @param value the maximum floating-point value in the data before quantization. * * @return itself * * @see #getMaxValue() * @see #setMinValue(double) */ public QuantizeOption setMaxValue(double value) { maxValue = value; return this; } /** * Sets the minimum floating-point value in the data * * @param value the mininum floating-point value in the data before quantization. * * @return itself * * @see #getMinValue() * @see #setMaxValue(double) */ public QuantizeOption setMinValue(double value) { minValue = value; return this; } /** * @deprecated The use of null values other than NaN for floating-point data types is not * standard in FITS. You should therefore avoid using this method to change it. Returns the * floating-point value that indicates a null datum in the image before * quantization is applied. Normally, the FITS standard is that NaN values indicate * null values in floating-point images. While this class allows using other * values also, they are not recommended since they are not supported by FITS in a standard * way. * * @param value the new floating-point value that represents a null value (missing data) in the * image before quantization. * * @return itself * * @see #setNullValue(double) */ public QuantizeOption setNullValue(double value) { nullValue = value; return this; } @Override public void setParameters(ICompressParameters parameters) { if (parameters instanceof QuantizeParameters) { this.parameters = (QuantizeParameters) parameters.copy(this); } else if (parameters instanceof BundledParameters) { BundledParameters bundle = (BundledParameters) parameters; for (int i = 0; i < bundle.size(); i++) { setParameters(bundle.get(i)); } } else if (compressOption != null) { compressOption.setParameters(parameters); } } /** * Sets the quantization resolution level to use for automatic quantization. For Gaussian noise the quantization * level is the standard deviation of the noise divided by this Q value. Thus Q values of a few will ensusre that * quantization retains just about all of the information contained in the noisy data. * * @param value The new Q value, defined as the number of quantized levels per standard deviation (for Gaussian * noise). * * @return itself * * @see #getQLevel() * @see #setBScale(double) */ public QuantizeOption setQlevel(double value) { config.qlevel = value; return this; } /** * Sets the seed value for the dither random generator * * @param value The seed value, as in ZDITHER0, normally a number between 1 and 10000 (inclusive). * * @return itself * * @see #setTileIndex(int) */ public QuantizeOption setSeed(long value) { config.seed = value; return this; } /** * Sets the tile index for which to initialize the random number generator with the given seed (i.e. * ZDITHER0 value). * * @param index The 0-based tile index * * @return itself * * @see #setSeed(long) */ public QuantizeOption setTileIndex(int index) { tileIndex = index; return this; } @Override public QuantizeOption setTileHeight(int value) { tileHeight = value; if (compressOption != null) { compressOption.setTileHeight(value); } return this; } @Override public QuantizeOption setTileWidth(int value) { tileWidth = value; if (compressOption != null) { compressOption.setTileWidth(value); } return this; } @Override public T unwrap(Class clazz) { if (clazz.isAssignableFrom(this.getClass())) { return clazz.cast(this); } if (compressOption != null) { if (clazz.isAssignableFrom(compressOption.getClass())) { return clazz.cast(compressOption); } } return null; } /** * Stores configuration in a way that can be shared and modified across enclosing option copies. * * @author Attila Kovacs * * @since 1.18 */ private static final class Config { private boolean centerOnZero; private boolean checkZero; private boolean dither; private boolean dither2; private double qlevel = Double.NaN; private long seed = 1L; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/quant/QuantizeProcessor.java000066400000000000000000000330151476377620500331260ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.quant; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.nio.ByteBuffer; import java.nio.DoubleBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import nom.tam.fits.compression.algorithm.api.ICompressor; /** * (for internal use) Qunatization step processor as part of compression. */ @SuppressWarnings({"javadoc", "deprecation"}) public class QuantizeProcessor { public static class DoubleQuantCompressor extends QuantizeProcessor implements ICompressor { private final ICompressor postCompressor; public DoubleQuantCompressor(QuantizeOption quantizeOption, ICompressor compressor) { super(quantizeOption); postCompressor = compressor; } @Override public boolean compress(DoubleBuffer buffer, ByteBuffer compressed) { IntBuffer intData = IntBuffer.wrap(new int[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]); double[] doubles = new double[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]; buffer.get(doubles); if (!this.quantize(doubles, intData)) { return false; } intData.rewind(); postCompressor.compress(intData, compressed); return true; } @Override public void decompress(ByteBuffer compressed, DoubleBuffer buffer) { IntBuffer intData = IntBuffer.wrap(new int[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]); postCompressor.decompress(compressed, intData); intData.rewind(); unquantize(intData, buffer); } } /** * TODO this is done very inefficient and should be refactored! */ public static class FloatQuantCompressor extends QuantizeProcessor implements ICompressor { private final ICompressor postCompressor; public FloatQuantCompressor(QuantizeOption quantizeOption, ICompressor postCompressor) { super(quantizeOption); this.postCompressor = postCompressor; } @Override public boolean compress(FloatBuffer buffer, ByteBuffer compressed) { float[] floats = new float[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]; double[] doubles = new double[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]; buffer.get(floats); for (int index = 0; index < doubles.length; index++) { doubles[index] = floats[index]; } IntBuffer intData = IntBuffer.wrap(new int[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]); if (!this.quantize(doubles, intData)) { return false; } intData.rewind(); postCompressor.compress(intData, compressed); return true; } @Override public void decompress(ByteBuffer compressed, FloatBuffer buffer) { IntBuffer intData = IntBuffer.wrap(new int[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]); postCompressor.decompress(compressed, intData); intData.rewind(); double[] doubles = new double[quantizeOption.getTileHeight() * quantizeOption.getTileWidth()]; DoubleBuffer doubleBuffer = DoubleBuffer.wrap(doubles); unquantize(intData, doubleBuffer); for (double d : doubles) { buffer.put((float) d); } } } private class BaseFilter extends PixelFilter { BaseFilter() { super(null); } @Override protected void nextPixel() { } @Override protected double toDouble(int pixel) { return (pixel + ROUNDING_HALF) * bScale + bZero; } @Override protected int toInt(double pixel) { return nint((pixel - bZero) / bScale + ROUNDING_HALF); } } private class DitherFilter extends PixelFilter { private static final int RANDOM_MULTIPLICATOR = 500; private int iseed = 0; private int nextRandom = 0; DitherFilter(long seed) { super(null); initialize(seed); } public void initialize(long ditherSeed) { iseed = (int) ((ditherSeed - 1) % RandomSequence.length()); initI1(); } private void initI1() { nextRandom = (int) (RandomSequence.get(iseed) * RANDOM_MULTIPLICATOR); } public double nextRandom() { return RandomSequence.get(nextRandom); } @Override protected void nextPixel() { nextRandom++; if (nextRandom >= RandomSequence.length()) { iseed++; if (iseed >= RandomSequence.length()) { iseed = 0; } initI1(); } } @Override protected double toDouble(int pixel) { return (pixel - nextRandom() + ROUNDING_HALF) * bScale + bZero; } @Override protected int toInt(double pixel) { return nint((pixel - bZero) / bScale + nextRandom() - ROUNDING_HALF); } } private class NullFilter extends PixelFilter { private final double nullValue; private final boolean isNaN; private final int nullValueIndicator; NullFilter(double nullValue, int nullValueIndicator, PixelFilter next) { super(next); this.nullValue = nullValue; isNaN = Double.isNaN(this.nullValue); this.nullValueIndicator = nullValueIndicator; } public final boolean isNull(double pixel) { return isNaN ? Double.isNaN(pixel) : nullValue == pixel; } @Override protected double toDouble(int pixel) { if (pixel == nullValueIndicator) { return nullValue; } return super.toDouble(pixel); } @Override protected int toInt(double pixel) { if (isNull(pixel)) { return nullValueIndicator; } return super.toInt(pixel); } } private class PixelFilter { private final PixelFilter next; protected PixelFilter(PixelFilter next) { this.next = next; } protected void nextPixel() { next.nextPixel(); } protected double toDouble(int pixel) { return next.toDouble(pixel); } protected int toInt(double pixel) { return next.toInt(pixel); } } private class ZeroFilter extends PixelFilter { ZeroFilter(PixelFilter next) { super(next); } @Override protected double toDouble(int pixel) { if (pixel == ZERO_VALUE) { return 0.0; } return super.toDouble(pixel); } @Override protected int toInt(double pixel) { if (pixel == 0.0) { return ZERO_VALUE; } return super.toInt(pixel); } } private static final double MAX_INT_AS_DOUBLE = Integer.MAX_VALUE; /** * number of reserved values, starting with */ private static final long N_RESERVED_VALUES = 10; private static final double ROUNDING_HALF = 0.5; /** * value used to represent zero-valued pixels */ private static final int ZERO_VALUE = Integer.MIN_VALUE + 2; private final boolean centerOnZero; private final PixelFilter pixelFilter; private double bScale; private double bZero; private Quantize quantize; protected final QuantizeOption quantizeOption; public QuantizeProcessor(QuantizeOption quantizeOption) { this.quantizeOption = quantizeOption; bScale = quantizeOption.getBScale(); bZero = quantizeOption.getBZero(); PixelFilter filter = null; boolean localCenterOnZero = quantizeOption.isCenterOnZero(); if (quantizeOption.isDither2()) { filter = new DitherFilter(quantizeOption.getSeed() + quantizeOption.getTileIndex()); localCenterOnZero = true; quantizeOption.setCheckZero(true); } else if (quantizeOption.isDither()) { filter = new DitherFilter(quantizeOption.getSeed() + quantizeOption.getTileIndex()); } else { filter = new BaseFilter(); } if (quantizeOption.isCheckZero()) { filter = new ZeroFilter(filter); } if (quantizeOption.isCheckNull()) { final NullFilter nullFilter = new NullFilter(quantizeOption.getNullValue(), quantizeOption.getBNull(), filter); filter = nullFilter; quantize = new Quantize(quantizeOption) { @Override protected int findNextValidPixelWithNullCheck(int nx, DoubleArrayPointer rowpix, int ii) { while (ii < nx && nullFilter.isNull(rowpix.get(ii))) { ii++; } return ii; } @Override protected boolean isNull(double d) { return nullFilter.isNull(d); } }; } else { quantize = new Quantize(quantizeOption); } pixelFilter = filter; centerOnZero = localCenterOnZero; } public Quantize getQuantize() { return quantize; } public boolean quantize(double[] doubles, IntBuffer quants) { boolean success = quantize.quantize(doubles, quantizeOption.getTileWidth(), quantizeOption.getTileHeight()); if (success) { calculateBZeroAndBscale(); quantize(DoubleBuffer.wrap(doubles, 0, quantizeOption.getTileWidth() * quantizeOption.getTileHeight()), quants); } return success; } public void quantize(final DoubleBuffer fdata, final IntBuffer intData) { while (fdata.hasRemaining()) { intData.put(pixelFilter.toInt(fdata.get())); pixelFilter.nextPixel(); } } public void unquantize(final IntBuffer intData, final DoubleBuffer fdata) { while (fdata.hasRemaining()) { fdata.put(pixelFilter.toDouble(intData.get())); pixelFilter.nextPixel(); } } private void calculateBZeroAndBscale() { bScale = quantizeOption.getBScale(); bZero = zeroCenter(); quantizeOption.setIntMinValue(nint((quantizeOption.getMinValue() - bZero) / bScale)); quantizeOption.setIntMaxValue(nint((quantizeOption.getMaxValue() - bZero) / bScale)); quantizeOption.setBZero(bZero); } private int nint(double x) { return x >= 0. ? (int) (x + ROUNDING_HALF) : (int) (x - ROUNDING_HALF); } private double zeroCenter() { final double minValue = quantizeOption.getMinValue(); final double maxValue = quantizeOption.getMaxValue(); double evaluatedBZero; if (!quantizeOption.isCheckNull() && !centerOnZero) { // don't have to check for nulls // return all positive values, if possible since some compression // algorithms either only work for positive integers, or are more // efficient. if ((maxValue - minValue) / bScale < MAX_INT_AS_DOUBLE - N_RESERVED_VALUES) { evaluatedBZero = minValue; // fudge the zero point so it is an integer multiple of bScale // This helps to ensure the same scaling will be performed if // the file undergoes multiple fpack/funpack cycles long iqfactor = (long) (evaluatedBZero / bScale + ROUNDING_HALF); evaluatedBZero = iqfactor * bScale; } else { /* center the quantized levels around zero */ evaluatedBZero = (minValue + maxValue) / 2.; } } else { // data contains null values or has be forced to center on zero // shift the range to be close to the value used to represent null // values evaluatedBZero = minValue - bScale * (Integer.MIN_VALUE + N_RESERVED_VALUES + 1); } return evaluatedBZero; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/quant/RandomSequence.java000066400000000000000000000063231476377620500323410ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.quant; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * A standard fixed random sequence to use for portable and reversible dither * implementations. This is a modified (improved) version of the random sequence * implementation in Appendix I of the FITS * 4.0 standard, using integer arithmetics for better performance -- but * still providing the same sequence as the original algorithm. * * @see QuantizeProcessor */ public final class RandomSequence { /** * DO NOT CHANGE THIS; used when quantizing real numbers */ private static final int N_RANDOM = 10000; private static final int RANDOM_FACTOR = 16807; /** * This is our cached fixed random sequence that we will use over and over, * but we defer initializing it until we actually need it. */ private static final double[] VALUES = new double[N_RANDOM]; /** * Static initialization for the fixed sequence of random values. */ static { long ival = 1L; for (int i = 0; i < N_RANDOM; i++) { ival = (ival * RANDOM_FACTOR) % Integer.MAX_VALUE; VALUES[i] = (double) ival / Integer.MAX_VALUE; } } /** We don't instantiate this class */ private RandomSequence() { } /** * Returns the ith random value from the sequence * * @param i * The index between 0 and {@link #length()} (exclusive). * @return The fixed uniform random deviate value at that index in the range * of 0.0 to 1.0 (exclusive). * @see #length() */ public static double get(int i) { return VALUES[i]; } /** * Returns the number of random values in the sequence. * * @return The number of random values available from the fixed sequence. */ public static int length() { return VALUES.length; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/quant/package-info.java000066400000000000000000000050671476377620500317600ustar00rootroot00000000000000/** *

* Quantization support for representing floating-point values with integers corresponding to discrete levels. While not * a compression alorithm in itself (hence you might also wonder why it's in a package on its own under compression * algorithms) quantization is nevertheless commonly used as a pre-sompression (or post-decompression step) with actual * algorithms, especially if the algorithms are designed for integer-only data. *

*

* Quantization is an inherently lossy process, so it will result in a lossy compression even when paired with a * lossless compression algorithm. However, it can significantly improve compression ratios when images have limited * dynamic range. For example a 2-byte integer quantization of double-precision values will provide 64k discrete levels * at 1/4th of the required storage space -- even before compression is applied. *

*

* The only class in here that users would typically interact with is * {@link nom.tam.fits.compression.algorithm.quant.QuantizeOption} or * {@link nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class)} to set options after a quantization * algorithm was selected for compressing an image HDU. *

*/ package nom.tam.fits.compression.algorithm.quant; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/rice/000077500000000000000000000000001476377620500263535ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/rice/BitBuffer.java000066400000000000000000000127131476377620500310720ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.rice; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.nio.ByteBuffer; /** * (for internal use) A bit wise reader writer around a {@link ByteBuffer}. * * @deprecated (for internal use) Its visibility may be reduced to the package level in the future. * * @author Ritchie */ @SuppressWarnings("javadoc") public class BitBuffer { private static final int BITS_OF_4_BYTES = 32; private static final int BYTE_MASK = 0xFF; private static final long INTEGER_MASK = 0xFFFFFFFFL; private static final int BITS_OF_1_BYTE = 8; private static final int BITS_OF_2_BYTES = 16; private static final int BITS_OF_3_BYTES = 24; private static final int BYTE_1_OF_INT = 0x000000FF; private static final int BYTE_2_OF_INT = 0x0000FF00; private static final int BYTE_3_OF_INT = 0x00FF0000; private static final int BYTE_4_OF_INT = 0xFF000000; private final ByteBuffer buffer; private long position; public BitBuffer(ByteBuffer writeBuffer) { buffer = writeBuffer; } public int bitbuffer() { return buffer.get((int) (position / BITS_OF_1_BYTE)); } void close() { if (position % BITS_OF_1_BYTE != 0) { putByte((byte) 0, (int) (BITS_OF_1_BYTE - position % BITS_OF_1_BYTE)); } buffer.position((int) (position / BITS_OF_1_BYTE)); } public int missingBitsInCurrentByte() { return (int) (BITS_OF_1_BYTE - position % BITS_OF_1_BYTE); } public void movePosition(int i) { position += i; } public void putByte(byte byteToAdd) { final int bytePosition = (int) (position / BITS_OF_1_BYTE); final int positionInByte = (int) (position % BITS_OF_1_BYTE); final byte old = (byte) (buffer.get(bytePosition) & (byte) ~(BYTE_MASK >>> positionInByte)); final int byteAsInt = byteToAdd & BYTE_MASK; buffer.put(bytePosition, (byte) (old | (byte) (byteAsInt >>> positionInByte))); if (positionInByte > 0) { buffer.put(bytePosition + 1, (byte) (byteAsInt << BITS_OF_1_BYTE - positionInByte)); } position += BITS_OF_1_BYTE; } public void putByte(byte byteToAdd, int bits) { final int bytePosition = (int) (position / BITS_OF_1_BYTE); final int positionInByte = (int) (position % BITS_OF_1_BYTE); final byte old = buffer.get(bytePosition); final int byteAsInt = BYTE_MASK & (byteToAdd & BYTE_MASK >>> BITS_OF_1_BYTE - bits) << BITS_OF_1_BYTE - bits; buffer.put(bytePosition, (byte) (BYTE_MASK & // (old & BYTE_MASK << BITS_OF_1_BYTE - positionInByte | byteAsInt >>> positionInByte))); if (BITS_OF_1_BYTE - positionInByte < bits) { buffer.put(bytePosition + 1, (byte) (BYTE_MASK & byteAsInt << BITS_OF_1_BYTE - positionInByte)); } position += bits; } /** * write out int value to the next 4 bytes of the buffer * * @param i integer to write */ public void putInt(int i) { putByte((byte) ((i & BYTE_4_OF_INT) >>> BITS_OF_3_BYTES)); putByte((byte) ((i & BYTE_3_OF_INT) >>> BITS_OF_2_BYTES)); putByte((byte) ((i & BYTE_2_OF_INT) >>> BITS_OF_1_BYTE)); putByte((byte) (i & BYTE_1_OF_INT)); } public void putInt(int i, int bits) { if (bits == 0) { return; } do { if (bits >= BITS_OF_1_BYTE) { putByte((byte) ((i & BYTE_MASK << bits - BITS_OF_1_BYTE) >>> bits - BITS_OF_1_BYTE & BYTE_MASK)); bits -= BITS_OF_1_BYTE; } else { putByte((byte) (i & BYTE_MASK >> -(bits - BITS_OF_1_BYTE)), bits); bits = 0; } } while (bits > 0); } public void putLong(long l, int bits) { if (bits == 0) { return; } do { if (bits >= BITS_OF_4_BYTES) { putInt((int) ((l & INTEGER_MASK << bits - BITS_OF_4_BYTES) >>> bits - BITS_OF_4_BYTES)); bits -= BITS_OF_4_BYTES; } else { putInt((int) (l & INTEGER_MASK >> -(bits - BITS_OF_4_BYTES)), bits); bits = 0; } } while (bits > 0); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/rice/RiceCompressOption.java000066400000000000000000000146601476377620500330140ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.rice; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.compression.algorithm.api.ICompressOption; import nom.tam.fits.compression.provider.param.api.ICompressParameters; import nom.tam.fits.compression.provider.param.rice.RiceCompressParameters; import nom.tam.util.type.ElementType; /** * Options to the Rice compression algorithm. When compressing tables and images using the Rice algorithm, users can * control how exactly the compression is perfomed. When reading compressed FITS files, these options will be set * automatically based on the header values recorded in the compressed HDU. * * @see nom.tam.image.compression.hdu.CompressedImageHDU#setCompressAlgorithm(String) * @see nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class) */ public class RiceCompressOption implements ICompressOption { /** the default block size to use in bytes */ public static final int DEFAULT_RICE_BLOCKSIZE = 32; /** the default BYTEPIX value */ public static final int DEFAULT_RICE_BYTEPIX = ElementType.INT.size(); /** Set of valid BYTEPIX values */ private static final int[] VALID_BYTEPIX = {1, 2, 4, 8}; /** Set of valid BLOCKSIZE values */ private static final int[] VALID_BLOCKSIZE = {16, 32}; /** The parameters that represent settings for this option in the FITS headers and/or compressed data columns */ private RiceCompressParameters parameters; /** Shared configuration across copies */ private final Config config; /** * Creates a new set of options for Rice compression. */ public RiceCompressOption() { config = new Config(); parameters = new RiceCompressParameters(this); } @Override public RiceCompressOption copy() { try { RiceCompressOption copy = (RiceCompressOption) clone(); copy.parameters = parameters.copy(copy); return copy; } catch (CloneNotSupportedException e) { throw new IllegalStateException("option could not be cloned", e); } } /** * Returns the currently set block size. * * @return the block size in bytes. * * @see #setBlockSize(int) */ public final int getBlockSize() { return config.blockSize; } /** * REturns the currently set BYTEPIX value * * @return the BYTEPIX value. * * @see #setBytePix(int) */ public final int getBytePix() { return config.bytePix; } @Override public RiceCompressParameters getCompressionParameters() { return parameters; } @Override public boolean isLossyCompression() { return false; } /** * Sets a new block size to use * * @param value the new block size in bytes * * @return itself * * @throws IllegalArgumentException if the value is not 16 or 32. * * @see #getBlockSize() */ public RiceCompressOption setBlockSize(int value) throws IllegalArgumentException { for (int i : VALID_BLOCKSIZE) { if (value == i) { config.blockSize = value; return this; } } throw new IllegalArgumentException("Invalid BYTEPIX value: " + value + " (must be 16 or 32)"); } /** * Sets a new BYTEPIX value to use. * * @param value the new BYTEPIX value. It is currently not checked for validity, so use * carefully. * * @return itself * * @throws IllegalArgumentException if the value is not 1, 2, 4, or 8. * * @see #getBytePix() */ public RiceCompressOption setBytePix(int value) throws IllegalArgumentException { for (int i : VALID_BYTEPIX) { if (value == i) { config.bytePix = value; return this; } } throw new IllegalArgumentException("Invalid BYTEPIX value: " + value + " (must be 1, 2, 4, or 8)"); } @Override public void setParameters(ICompressParameters parameters) { if (!(parameters instanceof RiceCompressParameters)) { throw new IllegalArgumentException("Wrong type of parameters: " + parameters.getClass().getName()); } this.parameters = (RiceCompressParameters) parameters.copy(this); } @Override public RiceCompressOption setTileHeight(int value) { return this; } @Override public RiceCompressOption setTileWidth(int value) { return this; } @Override public T unwrap(Class clazz) { if (clazz.isAssignableFrom(this.getClass())) { return clazz.cast(this); } return null; } /** * Stores configuration in a way that can be shared and modified across enclosing option copies. * * @author Attila Kovacs * * @since 1.18 */ private static final class Config { private int bytePix = DEFAULT_RICE_BYTEPIX; private int blockSize = DEFAULT_RICE_BLOCKSIZE; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/rice/RiceCompressor.java000066400000000000000000000474101476377620500321630ustar00rootroot00000000000000package nom.tam.fits.compression.algorithm.rice; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; import java.util.logging.Logger; import nom.tam.fits.compression.algorithm.api.ICompressor; import nom.tam.fits.compression.algorithm.quant.QuantizeProcessor.DoubleQuantCompressor; import nom.tam.fits.compression.algorithm.quant.QuantizeProcessor.FloatQuantCompressor; import nom.tam.util.FitsIO; import nom.tam.util.type.ElementType; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * (for internal use) The Rice compression algorithm. The original compression was designed by Rice, Yeh, and * Miller the code was written by Richard White at STScI at the STScI and included (ported to c and adapted) in cfitsio * by William Pence, NASA/GSFC. That code was then ported to java by R. van Nieuwenhoven. Later it was massively * refactored to harmonize the different compression algorithms and reduce the duplicate code pieces without obscuring * the algorithm itself as far as possible. * * @author Richard White * @author William Pence * @author Richard van Nieuwenhoven * * @param the genetic type of NIO buffer on which this compressor operates. */ @SuppressWarnings({"deprecation", "javadoc"}) public abstract class RiceCompressor implements ICompressor { public static class ByteRiceCompressor extends RiceCompressor { private ByteBuffer pixelBuffer; /** * Rice compression of byte streams with the default block size of 32. * * @since 1.19.1 * * @author Attila Kovacs */ public ByteRiceCompressor() { this(new RiceCompressOption()); } public ByteRiceCompressor(RiceCompressOption option) { super(option); } @Override public boolean compress(ByteBuffer buffer, ByteBuffer writeBuffer) { pixelBuffer = buffer; super.compress(buffer.limit(), pixelBuffer.get(pixelBuffer.position()), new BitBuffer(writeBuffer)); return true; } @Override public void decompress(ByteBuffer readBuffer, ByteBuffer buffer) { pixelBuffer = buffer; super.decompressBuffer(readBuffer, buffer.limit()); } @Override protected int nextPixel() { return pixelBuffer.get(); } @Override protected void nextPixel(int pixel) { pixelBuffer.put((byte) pixel); } } public static class DoubleRiceCompressor extends DoubleQuantCompressor { public DoubleRiceCompressor(RiceQuantizeCompressOption options) throws ClassCastException { super(options, new IntRiceCompressor((RiceCompressOption) options.getCompressOption())); } } public static class FloatRiceCompressor extends FloatQuantCompressor { public FloatRiceCompressor(RiceQuantizeCompressOption options) throws ClassCastException { super(options, new IntRiceCompressor((RiceCompressOption) options.getCompressOption())); } } public static class IntRiceCompressor extends RiceCompressor { private IntBuffer pixelBuffer; /** * Rice compression of 32-bit integer streams with the default block size of 32. * * @since 1.19.1 * * @author Attila Kovacs */ public IntRiceCompressor() { this(new RiceCompressOption()); } public IntRiceCompressor(RiceCompressOption option) { super(option); } @Override public boolean compress(IntBuffer buffer, ByteBuffer writeBuffer) { pixelBuffer = buffer; super.compress(buffer.limit(), pixelBuffer.get(pixelBuffer.position()), new BitBuffer(writeBuffer)); return true; } @Override public void decompress(ByteBuffer readBuffer, IntBuffer buffer) { pixelBuffer = buffer; super.decompressBuffer(readBuffer, buffer.limit()); } @Override protected int nextPixel() { return pixelBuffer.get(); } @Override protected void nextPixel(int pixel) { pixelBuffer.put(pixel); } } public static class ShortRiceCompressor extends RiceCompressor { private ShortBuffer pixelBuffer; /** * Rice compression of 16-bit integer streams with the default block size of 32. * * @since 1.19.1 * * @author Attila Kovacs */ public ShortRiceCompressor() { this(new RiceCompressOption()); } public ShortRiceCompressor(RiceCompressOption option) { super(option); } @Override public boolean compress(ShortBuffer buffer, ByteBuffer writeBuffer) { pixelBuffer = buffer; super.compress(buffer.limit(), pixelBuffer.get(pixelBuffer.position()), new BitBuffer(writeBuffer)); return true; } @Override public void decompress(ByteBuffer readBuffer, ShortBuffer buffer) { pixelBuffer = buffer; super.decompressBuffer(readBuffer, buffer.limit()); } @Override protected int nextPixel() { return pixelBuffer.get(); } @Override protected void nextPixel(int pixel) { pixelBuffer.put((short) pixel); } } /** * mask to convert a "unsigned" byte to a long. */ private static final long UNSIGNED_BYTE_MASK = 0xFFL; /** * mask to convert a "unsigned" short to a long. */ private static final long UNSIGNED_SHORT_MASK = 0xFFFFL; /** * mask to convert a "unsigned" int to a long. */ private static final long UNSIGNED_INTEGER_MASK = 0xFFFFFFFFL; /** * logger to log to. */ private static final Logger LOG = Logger.getLogger(RiceCompressor.class.getName()); private static final int BITS_OF_1_BYTE = 8; private static final int BITS_PER_BYTE = 8; private static final int BYTE_MASK = 0xff; private static final int FS_BITS_FOR_BYTE = 3; private static final int FS_BITS_FOR_INT = 5; private static final int FS_BITS_FOR_SHORT = 4; private static final int FS_MAX_FOR_BYTE = 6; private static final int FS_MAX_FOR_INT = 25; private static final int FS_MAX_FOR_SHORT = 14; /* * nonzero_count is lookup table giving number of bits in 8-bit values not including leading zeros used in * fits_rdecomp, fits_rdecomp_short and fits_rdecomp_byte. * * @formatter:off */ private static final int[] NONZERO_COUNT = {0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}; // @formatter:on private final int bBits; private final int bitsPerPixel; private final int blockSize; private final int fsBits; private final int fsMax; private RiceCompressor(RiceCompressOption option) throws UnsupportedOperationException { blockSize = option.getBlockSize(); if (option.getBytePix() == ElementType.BYTE.size()) { fsBits = FS_BITS_FOR_BYTE; fsMax = FS_MAX_FOR_BYTE; bitsPerPixel = FitsIO.BITS_OF_1_BYTE; } else if (option.getBytePix() == ElementType.SHORT.size()) { fsBits = FS_BITS_FOR_SHORT; fsMax = FS_MAX_FOR_SHORT; bitsPerPixel = FitsIO.BITS_OF_2_BYTES; } else if (option.getBytePix() == ElementType.INT.size()) { fsBits = FS_BITS_FOR_INT; fsMax = FS_MAX_FOR_INT; bitsPerPixel = FitsIO.BITS_OF_4_BYTES; } else { throw new UnsupportedOperationException("Implemented for 1/2/4 bytes only"); } /* * From bsize derive: FSBITS = # bits required to store FS FSMAX = maximum value for FS BBITS = bits/pixel for * direct coding */ bBits = 1 << fsBits; } /** *

* undo mapping and differencing Note that some of these operations will overflow the unsigned int arithmetic -- * that's OK, it all works out to give the right answers in the output file. *

*

* In java this is more complicated because of the missing unsigned integers. trying to simulate the behavior *

* * @param lastpix the current last pix value * @param diff the difference to "add" * * @return return the new lastpiy value */ private long undoMappingAndDifferencing(long lastpix, long diff) { diff &= UNSIGNED_INTEGER_MASK; if ((diff & 1) == 0) { diff = diff >>> 1; } else { diff = diff >>> 1 ^ UNSIGNED_INTEGER_MASK; } lastpix = diff + lastpix & UNSIGNED_INTEGER_MASK; nextPixel((int) lastpix); return lastpix; } /** * compress the integer tiledImageOperation on a rise compressed byte buffer. * * @param dataLength length of the data to compress * @param firstPixel the value of the first pixel * @param buffer the buffer to write to */ protected void compress(final int dataLength, int firstPixel, BitBuffer buffer) { /* the first difference will always be zero */ int lastpix = firstPixel; /* write out first int value to the first 4 bytes of the buffer */ buffer.putInt(firstPixel, bitsPerPixel); int thisblock = blockSize; for (int i = 0; i < dataLength; i += blockSize) { /* last block may be shorter */ if (dataLength - i < blockSize) { thisblock = dataLength - i; } /* * Compute differences of adjacent pixels and map them to unsigned values. Note that this may overflow the * integer variables -- that's OK, because we can recover when decompressing. If we were compressing shorts * or bytes, would want to do this arithmetic with short/byte working variables (though diff will still be * passed as an int.) compute sum of mapped pixel values at same time use double precision for sum to allow * 32-bit integer inputs */ long[] diff = new long[blockSize]; double pixelsum = 0.0; int nextpix; /* * tiledImageOperation for differences mapped to non-negative values */ for (int j = 0; j < thisblock; j++) { nextpix = nextPixel(); long pdiff = (nextpix - lastpix); diff[j] = (pdiff < 0 ? (pdiff << 1) ^ UNSIGNED_INTEGER_MASK : pdiff << 1) & UNSIGNED_INTEGER_MASK; pixelsum += diff[j]; lastpix = nextpix; } /* * compute number of bits to split from sum */ double dpsum = (pixelsum - thisblock / 2d - 1d) / thisblock; if (dpsum < 0) { dpsum = 0.0; } long psum = (long) dpsum >> 1; int fs; for (fs = 0; psum > 0; fs++) { // NOSONAR psum >>= 1; } /* * write the codes fsbits ID bits used to indicate split level */ if (fs >= fsMax) { /* * Special high entropy case when FS >= fsmax Just write pixel difference values directly, no Rice * coding at all. */ buffer.putInt(fsMax + 1, fsBits); for (int j = 0; j < thisblock; j++) { buffer.putLong(diff[j], bBits); } } else if (fs == 0 && pixelsum == 0) { // NOSONAR /* * special low entropy case when FS = 0 and pixelsum=0 (all pixels in block are zero.) Output a 0 and * return */ buffer.putInt(0, fsBits); } else { /* normal case: not either very high or very low entropy */ buffer.putInt(fs + 1, fsBits); int fsmask = (1 << fs) - 1; /* * local copies of bit buffer to improve optimization */ int bitsToGo = buffer.missingBitsInCurrentByte(); int bitBuffer = buffer.bitbuffer() >> bitsToGo; buffer.movePosition(bitsToGo - BITS_OF_1_BYTE); for (int j = 0; j < thisblock; j++) { int v = (int) diff[j]; int top = v >> fs; /* * top is coded by top zeros + 1 */ if (bitsToGo >= top + 1) { bitBuffer <<= top + 1; bitBuffer |= 1; bitsToGo -= top + 1; } else { bitBuffer <<= bitsToGo; buffer.putByte((byte) (bitBuffer & BYTE_MASK)); for (top -= bitsToGo; top >= BITS_OF_1_BYTE; top -= BITS_OF_1_BYTE) { buffer.putByte((byte) 0); } bitBuffer = 1; bitsToGo = BITS_OF_1_BYTE - 1 - top; } /* * bottom FS bits are written without coding code is output_nbits, moved into this routine to reduce * overheads This code potentially breaks if FS>24, so I am limiting FS to 24 by choice of FSMAX * above. */ if (fs > 0) { bitBuffer <<= fs; bitBuffer |= v & fsmask; bitsToGo -= fs; while (bitsToGo <= 0) { buffer.putByte((byte) (bitBuffer >> -bitsToGo & BYTE_MASK)); bitsToGo += BITS_OF_1_BYTE; } } } buffer.putByte((byte) (bitBuffer & BYTE_MASK), BITS_OF_1_BYTE - bitsToGo); } } buffer.close(); } /** * decompress the readbuffer and fill the pixelarray. * * @param readBuffer input buffer * @param nx the number of pixel to uncompress */ protected void decompressBuffer(final ByteBuffer readBuffer, final int nx) { /* first x bytes of input buffer contain the value of the first */ /* x byte integer value, without any encoding */ long lastpix = 0L; if (bitsPerPixel == ElementType.BYTE.bitPix()) { lastpix = readBuffer.get() & UNSIGNED_BYTE_MASK; } else if (bitsPerPixel == ElementType.SHORT.bitPix()) { lastpix = readBuffer.getShort() & UNSIGNED_SHORT_MASK; } else { // Must be (this.bitsPerPixel == ElementType.INT.bitPix()) lastpix = readBuffer.getInt() & UNSIGNED_INTEGER_MASK; } long b = readBuffer.get() & BYTE_MASK; /* bit buffer */ int nbits = BITS_PER_BYTE; /* number of bits remaining in b */ for (int i = 0; i < nx;) { /* get the FS value from first fsbits */ nbits -= fsBits; while (nbits < 0) { b = b << BITS_PER_BYTE | readBuffer.get() & BYTE_MASK; nbits += BITS_PER_BYTE; } long fs = (b >>> nbits) - 1L; b &= (1 << nbits) - 1; /* loop over the next block */ int imax = i + blockSize; if (imax > nx) { imax = nx; } if (fs < 0) { /* low-entropy case, all zero differences */ for (; i < imax; i++) { nextPixel((int) lastpix); } } else if (fs == fsMax) { /* high-entropy case, directly coded pixel values */ for (; i < imax; i++) { int k = bBits - nbits; long diff = b << k; for (k -= BITS_PER_BYTE; k >= 0; k -= BITS_PER_BYTE) { b = readBuffer.get() & BYTE_MASK; diff |= b << k; } if (nbits > 0) { b = readBuffer.get() & BYTE_MASK; diff |= b >>> -k; b &= (1 << nbits) - 1L; } else { b = 0; } lastpix = undoMappingAndDifferencing(lastpix, diff); } } else { /* normal case, Rice coding */ for (; i < imax; i++) { /* count number of leading zeros */ while (b == 0) { nbits += BITS_PER_BYTE; b = readBuffer.get() & BYTE_MASK; } long nzero = nbits - NONZERO_COUNT[(int) (b & BYTE_MASK)]; nbits -= nzero + 1; /* flip the leading one-bit */ b ^= 1 << nbits; /* get the FS trailing bits */ nbits -= fs; while (nbits < 0) { b = b << BITS_PER_BYTE | readBuffer.get() & BYTE_MASK; nbits += BITS_PER_BYTE; } long diff = nzero << fs | b >> nbits; b &= (1 << nbits) - 1L; lastpix = undoMappingAndDifferencing(lastpix, diff); } } } if (readBuffer.limit() > readBuffer.position()) { LOG.warning("decompressing left over some extra bytes got: " + readBuffer.limit() + " but needed only " + readBuffer.position()); } } protected abstract int nextPixel(); protected abstract void nextPixel(int pixel); } RiceQuantizeCompressOption.java000066400000000000000000000063601476377620500344540ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/ricepackage nom.tam.fits.compression.algorithm.rice; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.compression.algorithm.quant.QuantizeOption; /** * @deprecated (for internal use) This class should not be exposed to users. *

* Options to the Rice compression algorithm when the compression includes quantization. When * compressing tables and images using the Rice algorithm, including quantization, users can control how * exactly the compression and quantization are perfomed. When reading compressed FITS files, these * options will be set automatically based on the header values recorded in the compressed HDU. *

* * @see nom.tam.image.compression.hdu.CompressedImageHDU#setCompressAlgorithm(String) * @see nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class) * @see RiceCompressOption */ @Deprecated public class RiceQuantizeCompressOption extends QuantizeOption { /** * Creates a new set of options for Rice compression with quantization, initialized to default values. */ public RiceQuantizeCompressOption() { super(new RiceCompressOption()); } /** * Creates a new set of options for Rice compression with quantization, using the specified option to the Rice * (de)compression, and initializing the qunatization options with default values. * * @param compressOption The Rice compression options to use */ public RiceQuantizeCompressOption(RiceCompressOption compressOption) { super(compressOption); } /** * Returns the options that are specific to the Rice compression algorithm (without quantization). * * @return the included options to the Rice compression algorithm * * @see #getCompressOption(Class) */ public RiceCompressOption getRiceCompressOption() { return (RiceCompressOption) super.getCompressOption(); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/rice/package-info.java000066400000000000000000000045301476377620500315440ustar00rootroot00000000000000/** *

* The Rice comppression algorithm and its options. This lossless compression algorithm was designed by Rice, Yeh, and * Miller the code was written by Richard White at STSc at the STScI and included (ported to c and adapted) in cfitsio * by William Pence, NASA/GSFC. That code was then ported to java by R. van Nieuwenhoven. Later it was massively * refactored to harmonize the different compression algorithms and reduce the duplicate code pieces without obscuring * the algorithm itself as far as possible. It's weird that it's a package by itself, but that's life. *

*

* The only classes in here that users would typically interact with are * {@link nom.tam.fits.compression.algorithm.rice.RiceCompressOption} and potentially * {@link nom.tam.fits.compression.algorithm.rice.RiceQuantizeCompressOption}, e.g. via * {@link nom.tam.image.compression.hdu.CompressedImageHDU#getCompressOption(Class)} to set options after Rice * compression was selected for compressing an image HDU. *

*/ package nom.tam.fits.compression.algorithm.rice; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/uncompressed/000077500000000000000000000000001476377620500301405ustar00rootroot00000000000000NoCompressCompressor.java000066400000000000000000000122761476377620500351010ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/uncompressedpackage nom.tam.fits.compression.algorithm.uncompressed; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.DoubleBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.LongBuffer; import java.nio.ShortBuffer; import nom.tam.fits.compression.algorithm.api.ICompressor; import nom.tam.util.type.ElementType; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * (for internal use) This compression algorithm will just copy the input to the output and do nothing at all. * * @param the buffer type of the pixel data */ @SuppressWarnings("javadoc") public abstract class NoCompressCompressor implements ICompressor { public static class ByteNoCompressCompressor extends NoCompressCompressor { @Override public boolean compress(ByteBuffer pixelData, ByteBuffer compressed) { compressed.put(pixelData); return true; } @Override public void decompress(ByteBuffer compressed, ByteBuffer pixelData) { pixelData.put(compressed); } } public static class DoubleNoCompressCompressor extends NoCompressCompressor { @Override public boolean compress(DoubleBuffer pixelData, ByteBuffer compressed) { int size = pixelData.remaining(); compressed.asDoubleBuffer().put(pixelData); compressed.position(compressed.position() + size * ElementType.DOUBLE.size()); return true; } @Override public void decompress(ByteBuffer compressed, DoubleBuffer pixelData) { pixelData.put(compressed.asDoubleBuffer()); } } public static class FloatNoCompressCompressor extends NoCompressCompressor { @Override public boolean compress(FloatBuffer pixelData, ByteBuffer compressed) { int size = pixelData.remaining(); compressed.asFloatBuffer().put(pixelData); compressed.position(compressed.position() + size * ElementType.FLOAT.size()); return true; } @Override public void decompress(ByteBuffer compressed, FloatBuffer pixelData) { pixelData.put(compressed.asFloatBuffer()); } } public static class IntNoCompressCompressor extends NoCompressCompressor { @Override public boolean compress(IntBuffer pixelData, ByteBuffer compressed) { int size = pixelData.remaining(); compressed.asIntBuffer().put(pixelData); compressed.position(compressed.position() + size * ElementType.INT.size()); return true; } @Override public void decompress(ByteBuffer compressed, IntBuffer pixelData) { pixelData.put(compressed.asIntBuffer()); } } public static class LongNoCompressCompressor extends NoCompressCompressor { @Override public boolean compress(LongBuffer pixelData, ByteBuffer compressed) { int size = pixelData.remaining(); compressed.asLongBuffer().put(pixelData); compressed.position(compressed.position() + size * ElementType.LONG.size()); return true; } @Override public void decompress(ByteBuffer compressed, LongBuffer pixelData) { pixelData.put(compressed.asLongBuffer()); } } public static class ShortNoCompressCompressor extends NoCompressCompressor { @Override public boolean compress(ShortBuffer pixelData, ByteBuffer compressed) { int size = pixelData.remaining(); compressed.asShortBuffer().put(pixelData); compressed.position(compressed.position() + size * ElementType.SHORT.size()); return true; } @Override public void decompress(ByteBuffer compressed, ShortBuffer pixelData) { pixelData.put(compressed.asShortBuffer()); } } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/algorithm/uncompressed/package-info.java000066400000000000000000000030161476377620500333270ustar00rootroot00000000000000/** * ( for internal use) Compression algorithm, for non compressing at all. It's weird, I know, and it's weirder * that it's a package by itself, but that's life. */ package nom.tam.fits.compression.algorithm.uncompressed; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/000077500000000000000000000000001476377620500252755ustar00rootroot00000000000000CompressorControlNameComputer.java000066400000000000000000000114161476377620500341010ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/providerpackage nom.tam.fits.compression.provider; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.header.Compression; /** *

* (for internal use) Finds the name of the appropriate tile compressor class name given the algorithm used to * quantize and compress the tile and the type of data the tile contains. *

* The name of the class is built of four parts: *
    *
  • the capitalized simple name of the base type of the elements in the tile (like Int, Long etc.);
  • *
  • if a known quantize algorithm is used, the word "Quant", the word "Unknown" if the quantize algorithm is not * recognized, nothing (i.e. the empty string) if it is null;
  • *
  • the short name of the compression algorithm to use (Rice, PLIO, Gzip etc.) or the word "Unknown" if the algorithm * is not supported;
  • *
  • the suffix "Compressor"
  • *
*

* Following exception to above rules exist: *

*
    *
  • If the primitive type is double or float, the quantize algorithm is ignored (as if it were specified as * null)
  • *
* See the associated unit tests for concrete examples. */ @SuppressWarnings("javadoc") public class CompressorControlNameComputer { private static final String COMPRESSOR_CLASS_SUFFIX = "Compressor"; private static String standardizeBaseType(String simpleName) { return Character.toUpperCase(simpleName.charAt(0)) + simpleName.substring(1).toLowerCase(); } private static String standardizeCompressionAlgorithm(String compressionAlgorithm) { if (Compression.ZCMPTYPE_RICE_1.equalsIgnoreCase(compressionAlgorithm) || // Compression.ZCMPTYPE_RICE_ONE.equalsIgnoreCase(compressionAlgorithm)) { return "Rice"; } if (Compression.ZCMPTYPE_PLIO_1.equalsIgnoreCase(compressionAlgorithm)) { return "PLIO"; } if (Compression.ZCMPTYPE_HCOMPRESS_1.equalsIgnoreCase(compressionAlgorithm)) { return "H"; } if (Compression.ZCMPTYPE_GZIP_2.equalsIgnoreCase(compressionAlgorithm)) { return "GZip2"; } if (Compression.ZCMPTYPE_GZIP_1.equalsIgnoreCase(compressionAlgorithm)) { return "GZip"; } if (Compression.ZCMPTYPE_NOCOMPRESS.equalsIgnoreCase(compressionAlgorithm)) { return "NoCompress"; } return "Unknown"; } private static String standardizeQuantAlgorithm(String quantAlgorithm) { if (quantAlgorithm != null) { if (Compression.ZQUANTIZ_NO_DITHER.equalsIgnoreCase(quantAlgorithm) || // Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_1.equalsIgnoreCase(quantAlgorithm) || // Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_2.equalsIgnoreCase(quantAlgorithm)) { return "Quant"; } return "Unknown"; } return ""; } public CompressorControlNameComputer() { super(); } public String createCompressorClassName(String quantAlgorithm, String compressionAlgorithm, Class baseType) { StringBuilder className = new StringBuilder(); className.append(standardizeBaseType(baseType.getSimpleName())); if (className.indexOf(Float.class.getSimpleName()) < 0 && className.indexOf(Double.class.getSimpleName()) < 0) { className.append(standardizeQuantAlgorithm(quantAlgorithm)); } className.append(standardizeCompressionAlgorithm(compressionAlgorithm)); className.append(COMPRESSOR_CLASS_SUFFIX); return className.toString(); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/CompressorProvider.java000066400000000000000000000373431476377620500320210ustar00rootroot00000000000000package nom.tam.fits.compression.provider; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.util.ServiceLoader; import java.util.logging.Level; import java.util.logging.Logger; import nom.tam.fits.FitsException; import nom.tam.fits.compression.algorithm.api.ICompressOption; import nom.tam.fits.compression.algorithm.api.ICompressor; import nom.tam.fits.compression.algorithm.api.ICompressorControl; import nom.tam.fits.compression.algorithm.gzip.GZipCompressor.ByteGZipCompressor; import nom.tam.fits.compression.algorithm.gzip.GZipCompressor.DoubleGZipCompressor; import nom.tam.fits.compression.algorithm.gzip.GZipCompressor.FloatGZipCompressor; import nom.tam.fits.compression.algorithm.gzip.GZipCompressor.IntGZipCompressor; import nom.tam.fits.compression.algorithm.gzip.GZipCompressor.LongGZipCompressor; import nom.tam.fits.compression.algorithm.gzip.GZipCompressor.ShortGZipCompressor; import nom.tam.fits.compression.algorithm.gzip2.GZip2Compressor.ByteGZip2Compressor; import nom.tam.fits.compression.algorithm.gzip2.GZip2Compressor.DoubleGZip2Compressor; import nom.tam.fits.compression.algorithm.gzip2.GZip2Compressor.FloatGZip2Compressor; import nom.tam.fits.compression.algorithm.gzip2.GZip2Compressor.IntGZip2Compressor; import nom.tam.fits.compression.algorithm.gzip2.GZip2Compressor.LongGZip2Compressor; import nom.tam.fits.compression.algorithm.gzip2.GZip2Compressor.ShortGZip2Compressor; import nom.tam.fits.compression.algorithm.hcompress.HCompressor.ByteHCompressor; import nom.tam.fits.compression.algorithm.hcompress.HCompressor.DoubleHCompressor; import nom.tam.fits.compression.algorithm.hcompress.HCompressor.FloatHCompressor; import nom.tam.fits.compression.algorithm.hcompress.HCompressor.IntHCompressor; import nom.tam.fits.compression.algorithm.hcompress.HCompressor.ShortHCompressor; import nom.tam.fits.compression.algorithm.hcompress.HCompressorQuantizeOption; import nom.tam.fits.compression.algorithm.plio.PLIOCompress.BytePLIOCompressor; import nom.tam.fits.compression.algorithm.plio.PLIOCompress.IntPLIOCompressor; import nom.tam.fits.compression.algorithm.plio.PLIOCompress.ShortPLIOCompressor; import nom.tam.fits.compression.algorithm.quant.QuantizeOption; import nom.tam.fits.compression.algorithm.quant.QuantizeProcessor.DoubleQuantCompressor; import nom.tam.fits.compression.algorithm.quant.QuantizeProcessor.FloatQuantCompressor; import nom.tam.fits.compression.algorithm.rice.RiceCompressor.ByteRiceCompressor; import nom.tam.fits.compression.algorithm.rice.RiceCompressor.DoubleRiceCompressor; import nom.tam.fits.compression.algorithm.rice.RiceCompressor.FloatRiceCompressor; import nom.tam.fits.compression.algorithm.rice.RiceCompressor.IntRiceCompressor; import nom.tam.fits.compression.algorithm.rice.RiceCompressor.ShortRiceCompressor; import nom.tam.fits.compression.algorithm.rice.RiceQuantizeCompressOption; import nom.tam.fits.compression.algorithm.uncompressed.NoCompressCompressor.ByteNoCompressCompressor; import nom.tam.fits.compression.algorithm.uncompressed.NoCompressCompressor.DoubleNoCompressCompressor; import nom.tam.fits.compression.algorithm.uncompressed.NoCompressCompressor.FloatNoCompressCompressor; import nom.tam.fits.compression.algorithm.uncompressed.NoCompressCompressor.IntNoCompressCompressor; import nom.tam.fits.compression.algorithm.uncompressed.NoCompressCompressor.LongNoCompressCompressor; import nom.tam.fits.compression.algorithm.uncompressed.NoCompressCompressor.ShortNoCompressCompressor; import nom.tam.fits.compression.provider.api.ICompressorProvider; import nom.tam.fits.compression.provider.param.api.ICompressHeaderParameter; import nom.tam.fits.compression.provider.param.api.ICompressParameters; import nom.tam.fits.compression.provider.param.base.CompressParameters; import nom.tam.fits.compression.provider.param.hcompress.HCompressParameters; import nom.tam.fits.compression.provider.param.rice.RiceCompressParameters; /** * (for internal use) Standard implementation of the {@code ICompressorProvider} interface. */ @SuppressWarnings({"javadoc", "deprecation"}) public class CompressorProvider implements ICompressorProvider { /** * private implementation of the tile compression provider, all is based on the option based constructor of the * compressors. */ protected static class TileCompressorControl implements ICompressorControl { private final Constructor>[] constructors; private Class optionClass; private Class quantType; @SuppressWarnings("unchecked") protected TileCompressorControl(Class compressorClass) { constructors = (Constructor>[]) compressorClass.getConstructors(); for (Constructor> c : constructors) { if (c.getParameterTypes().length == 1) { optionClass = (Class) c.getParameterTypes()[0]; break; } } } /** * Sets the floating-point type to quantize to use for this tile compressor. * * @param floatingPointType Floating-point primitive type to quantize. Must be either double.class * or else float.class. * * @return itself * * @since 1.18 */ protected TileCompressorControl setQuantType(Class floatingPointType) { quantType = floatingPointType; return this; } @Override public boolean compress(Buffer in, ByteBuffer out, ICompressOption option) { try { return newCompressor(option).compress(in, out); } catch (Exception e) { LOG.log(Level.FINE, "could not compress using " + constructors[0].getName() + " must fallback to other compression method", e); return false; } } @Override public void decompress(ByteBuffer in, Buffer out, ICompressOption option) { try { newCompressor(option).decompress(in, out); } catch (Exception e) { throw new IllegalStateException("could not decompress " + constructors[0].getName(), e); } } @Override public ICompressOption option() { ICompressOption option = null; if (optionClass != null) { try { option = optionClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new IllegalStateException("could not instantiate option class for " + constructors[0].getName(), e); } } if (option == null) { option = NULL_OPTION; } if (quantType != null) { return new QuantizeOption(option); } return option; } @SuppressWarnings({"unchecked", "rawtypes"}) private ICompressor newCompressor(ICompressOption option) throws FitsException, InstantiationException, IllegalAccessException, InvocationTargetException { ICompressor compressor = null; QuantizeOption quantOption = null; if (option instanceof QuantizeOption) { quantOption = (QuantizeOption) option; option = quantOption.getCompressOption(); } if (option == NULL_OPTION) { option = null; } try { for (Constructor> c : constructors) { Class[] parms = c.getParameterTypes(); if (parms.length == 0 && option == null) { // Use constructor without special options... compressor = c.newInstance(); break; } if (parms.length == 1 && option != null) { // Use constructor with the option Class p = (Class) parms[0]; if (quantOption != null && p.isAssignableFrom(quantOption.getClass())) { compressor = c.newInstance(quantOption); quantOption = null; // Don't wrap in a quantizer below... break; } if (p.isAssignableFrom(option.getClass())) { compressor = c.newInstance(option); break; } } } if (compressor == null) { throw new FitsException("Could not instantiate (de)compressor for the specified options"); } if (quantOption != null && quantType != null) { if (quantType.equals(double.class)) { return (ICompressor) new DoubleQuantCompressor(quantOption, (ICompressor) compressor); } if (quantType.equals(float.class)) { return (ICompressor) new FloatQuantCompressor(quantOption, (ICompressor) compressor); } } return compressor; } catch (Exception e) { e.printStackTrace(); } return null; } } private static final ICompressOption NULL_OPTION = new ICompressOption() { @Override public ICompressOption copy() { return this; } @Override public ICompressParameters getCompressionParameters() { return NULL_PARAMETERS; } @Override public boolean isLossyCompression() { return false; } @Override public void setParameters(ICompressParameters parameters) { } @Override public ICompressOption setTileHeight(int value) { return this; } @Override public ICompressOption setTileWidth(int value) { return this; } @Override public T unwrap(Class clazz) { return clazz.isAssignableFrom(this.getClass()) ? clazz.cast(this) : null; } }; private static final ICompressParameters NULL_PARAMETERS = new CompressParameters() { @Override protected ICompressHeaderParameter[] headerParameters() { return new ICompressHeaderParameter[0]; } @Override public ICompressParameters copy(ICompressOption option) { return this; } }; // @formatter:off private static final Class[][] AVAILABLE_COMPRESSORS = {// {ByteRiceCompressor.class, RiceCompressParameters.class}, // {ShortRiceCompressor.class, RiceCompressParameters.class}, // {IntRiceCompressor.class, RiceCompressParameters.class}, // {FloatRiceCompressor.class, RiceQuantizeCompressOption.class}, // {DoubleRiceCompressor.class, RiceQuantizeCompressOption.class}, // {BytePLIOCompressor.class}, // {ShortPLIOCompressor.class}, // {IntPLIOCompressor.class}, // {ByteHCompressor.class, HCompressParameters.class}, // {ShortHCompressor.class, HCompressParameters.class}, // {IntHCompressor.class, HCompressParameters.class}, // {FloatHCompressor.class, HCompressorQuantizeOption.class}, // {DoubleHCompressor.class, HCompressorQuantizeOption.class}, // {ByteGZip2Compressor.class}, // {ShortGZip2Compressor.class}, // {IntGZip2Compressor.class}, // {FloatGZip2Compressor.class}, // {DoubleGZip2Compressor.class}, // {LongGZip2Compressor.class}, // {ByteGZipCompressor.class}, // {ShortGZipCompressor.class}, // {IntGZipCompressor.class}, // {LongGZipCompressor.class}, // {FloatGZipCompressor.class}, // {DoubleGZipCompressor.class}, // {ByteNoCompressCompressor.class}, // {ShortNoCompressCompressor.class}, // {IntNoCompressCompressor.class}, // {LongNoCompressCompressor.class}, // {FloatNoCompressCompressor.class}, // {DoubleNoCompressCompressor.class}}; // @formatter:on private static final CompressorControlNameComputer NAME_COMPUTER = new CompressorControlNameComputer(); /** * logger to log to. */ private static final Logger LOG = Logger.getLogger(CompressorProvider.class.getName()); public static ICompressorControl findCompressorControl(String quantAlgorithm, String compressionAlgorithm, Class baseType) { for (ICompressorProvider iTileCompressorProvider : ServiceLoader.load(ICompressorProvider.class, Thread.currentThread().getContextClassLoader())) { ICompressorControl result = iTileCompressorProvider.createCompressorControl(quantAlgorithm, compressionAlgorithm, baseType); if (result != null) { return result; } } return new CompressorProvider().createCompressorControl(quantAlgorithm, compressionAlgorithm, baseType); } @Override public ICompressorControl createCompressorControl(String quantAlgorithm, String compressionAlgorithm, Class baseType) { Class quantType = null; if (quantAlgorithm != null) { // Standard compression via 32-bit integers... if (baseType.equals(double.class) || baseType.equals(float.class)) { quantType = baseType; baseType = int.class; quantAlgorithm = null; } } String className = NAME_COMPUTER.createCompressorClassName(quantAlgorithm, compressionAlgorithm, baseType); for (Class[] types : AVAILABLE_COMPRESSORS) { Class compressorClass = types[0]; if (compressorClass.getSimpleName().equals(className)) { TileCompressorControl tc = new TileCompressorControl(compressorClass); tc.setQuantType(quantType); return tc; } } return null; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/api/000077500000000000000000000000001476377620500260465ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/api/ICompressorProvider.java000066400000000000000000000041271476377620500326750ustar00rootroot00000000000000package nom.tam.fits.compression.provider.api; import nom.tam.fits.compression.algorithm.api.ICompressorControl; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * (for internal use) Service loader Interface to provide compression algorithms to fits. */ public interface ICompressorProvider { /** * @return the {@code ICompressorControl} to use for the specified quantize and compression * algorithms and base type. * * @param quantAlgorithm the quantification algorithm to use or null if none * @param compressionAlgorithm the compression algorithm to use * @param baseType the base type of the data to (de)compress. */ ICompressorControl createCompressorControl(String quantAlgorithm, String compressionAlgorithm, Class baseType); } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/api/package-info.java000066400000000000000000000030601476377620500312340ustar00rootroot00000000000000/** * (for internal use) Defines an interface for looking up compression algorithms. It's weird enough that it's in * its own package, rather than being part of the core compression package, but that's life. */ package nom.tam.fits.compression.provider.api; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/package-info.java000066400000000000000000000030441476377620500304650ustar00rootroot00000000000000/** * (for internal use) Finding the right compression classes for every occasion. It's weird that it is in its own * package, rather than being part of hte base compression package, but that's life. */ package nom.tam.fits.compression.provider; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/000077500000000000000000000000001476377620500263755ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/api/000077500000000000000000000000001476377620500271465ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/api/HeaderAccess.java000066400000000000000000000062521476377620500323300ustar00rootroot00000000000000package nom.tam.fits.compression.provider.param.api; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.Header; import nom.tam.fits.HeaderCard; import nom.tam.fits.HeaderCardException; /** * (for internal use) Access to FITS header values with runtime exceptions only. Regular header access throws * {@link HeaderCardException}s, which are hard exceptions. They really should have been softer runtime exceptions from * the start, but unfortunately that was choice this library made a very long time ago, and we therefore stick to it, at * least until the next major code revision (major version 2 at the earliest). So this class provides an alternative * access to headers converting any HeaderCardExceptions to {@link IllegalArgumentException}. * * @see Header * * @deprecated This class serves no purpose since 1.19. Will remove in some future. Prior to 1.19 {@link Header} threw * hard {@link HeaderCardException}, and this class was added so we can convert these into soft * {@link IllegalArgumentException} instead. However, now that we demoted * HeaderCardException to be soft exceptions itself, there is no reason to convert. It just * adds confusion. */ public class HeaderAccess implements IHeaderAccess { private final Header header; /** *

* Creates a new access to modifying a {@link HeaderCard} without the hard exceptions that HeaderCard * may throw. *

* * @param header the FITS header we wish to access and modify */ public HeaderAccess(Header header) { this.header = header; } /** * Returns the header that this class is providing access to. * * @return the Header that we access through this class * * @since 1.19 */ @Override public final Header getHeader() { return header; } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/api/HeaderCardAccess.java000066400000000000000000000120501476377620500331130ustar00rootroot00000000000000package nom.tam.fits.compression.provider.param.api; import nom.tam.fits.Header; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.HeaderCard; import nom.tam.fits.HeaderCardException; import nom.tam.fits.header.IFitsHeader; /** *

* (for internal use / no longer used) Access to a specific FITS header card with runtime exceptions only. * Regular modifications to {@link HeaderCard} may throw {@link HeaderCardException}s, which are hard exceptions. They * really should have been softer runtime exceptions from the start, but unfortunately that was choice this library made * a very long time ago, and we therefore stick to it, at least until the next major code revision (major version 2 at * the earliest). So this class provides an alternative access to a header card converting any * HeaderCardExceptions to {@link IllegalArgumentException}. *

*

* Unlike {@link HeaderAccess} this class operates on single cards. Methods that specify a keywords are applied to the * selected card if and only if the keyword matches that of the card's keyword. *

* * @see Header * * @deprecated This class serves no purpose since 1.19. Will remove in some future. Prior to 1.19 {@link Header} threw * hard {@link HeaderCardException}, and this class was added so we can convert these into soft * {@link IllegalArgumentException} instead. However, now that we demoted * HeaderCardException to be soft exceptions itself, there is no reason to convert. It just * adds confusion. */ public class HeaderCardAccess implements IHeaderAccess { private final HeaderCard headerCard; /** *

* Creates a new access to modifying a {@link HeaderCard} without the hard exceptions that HeaderCard * may throw. *

*

* Unlike {@link HeaderAccess} this class operates on single cards. Methods that specify a keywords are applied to * the selected card if and only if the keyword matches that of the card's keyword. *

* * @param headerCard the FITS keyword of the card we will provide access to * @param value the initial string value for the card (assuming the keyword allows string * values). * * @throws IllegalArgumentException if the header card could not be created */ public HeaderCardAccess(IFitsHeader headerCard, String value) throws IllegalArgumentException { try { this.headerCard = new HeaderCard(headerCard.key(), value, null); } catch (HeaderCardException e) { throw new IllegalArgumentException("header card could not be created"); } } @Override public final Header getHeader() { Header header = new Header(); header.addLine(headerCard); return header; } /** * Returns the header card that this class is providing access to. * * @return the Header card that we access through this class * * @since 1.19 */ public final HeaderCard getHeaderCard() { return headerCard; } @Override public void addValue(IFitsHeader key, int value) { if (headerCard.getKey().equals(key.key())) { headerCard.setValue(value); } } @Override public void addValue(IFitsHeader key, String value) { if (headerCard.getKey().equals(key.key())) { headerCard.setValue(value); } } @Override public HeaderCard findCard(IFitsHeader key) { return findCard(key.key()); } @Override public HeaderCard findCard(String key) { if (headerCard.getKey().equals(key)) { return headerCard; } return null; } } ICompressColumnParameter.java000066400000000000000000000130341476377620500346560ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/apipackage nom.tam.fits.compression.provider.param.api; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** *

* (for internal use) Compression parameters that are stored in the table along with the compressed data. Each * parameter is associated to a comlumn in the table, and the parameter takes the value that is stored in the same row * as the compressed data themselves. *

*

* It is possible to make independent copies of a set of such parameters, e.g. for parallel processing. In such cases * all copies share their underlying column data with the original, so changing the sotrage array of column data in * either the original or any of its decendants will affect the original and all decendans equally. *

*/ public interface ICompressColumnParameter extends ICompressParameter { /** * Returns the data column, in which each entry stores the parameter value for the compressed data of the * corresponding row in the table. * * @return The array that contains the data for each compressed row. * * @see #setColumnData(Object, int) * @see #getValueFromColumn(int) * @see #setValueInColumn(int) * * @since 1.18 */ Object getColumnData(); /** * @deprecated Provided for back compatibility only. Use {@link #getColumnData()} instead. * * @return The array that contains the data for each compressed row. */ @Deprecated default Object column() { return getColumnData(); } /** * @deprecated Provided for back compatibility only. Use {@link #getColumnData()} instead. * * @return The array that contains the data for each compressed row. */ @Deprecated default Object initializedColumn() { return getColumnData(); } /** * Sets new parameter data for each compressed row to be stored along as a separate parameter column in the * compressed table. To discard prior column data with no replacement, you can call this as * setColumnData(null, 0). * * @param column The array that contains the data for each compressed row. If not null the * size parameter is ignored, and the size of the array is used instead. * @param size The number of compressed rows in the table, if the column argument is * null. If size is zero or negative, any prior column data will be * discarded and null will be set. * * @see #getColumnData() */ void setColumnData(Object column, int size); /** * @deprecated Provided for back compatibility only. Use {@link #setColumnData(Object, int)} instead. * * @param column The array that contains the data for each compressed row. If not null the * size parameter is ignored, and the size of the array is used instead. * @param size The number of compressed rows in the table, if the column argument is * null */ @Deprecated default void column(Object column, int size) { setColumnData(column, size); } /** * Updates the associated compression options to use the parameter value defined to the compressed tile of the * specified index. * * @param index the tile index, a.k.a. row index in the compressed data table. * * @see #setValueInColumn(int) * @see #setColumnData(Object, int) */ void getValueFromColumn(int index); /** * Stores the current parameter value of the associated compression options for the tile of the specified index. * * @param index the tile index, a.k.a. row index in the compressed data table. * * @see #getValueFromColumn(int) * @see #setColumnData(Object, int) */ void setValueInColumn(int index); /** * @deprecated Provided for back compatibility only. Use {@link #setValueInColumn(int)} instead. * * @param index the tile index, a.k.a. row index in the compressed data table. */ @Deprecated default void setValueFromColumn(int index) { setValueInColumn(index); } } ICompressHeaderParameter.java000066400000000000000000000056311476377620500346150ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/apipackage nom.tam.fits.compression.provider.param.api; import nom.tam.fits.Header; import nom.tam.fits.HeaderCardException; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * (for internal use) Compression parameter that must be stored along the header meta data of the hdu. */ public interface ICompressHeaderParameter extends ICompressParameter { /** * get the value from the header and set it in the compression option. * * @param header the header of the hdu * * @deprecated Use {@link #getValueFromHeader(Header)} instead. */ default void getValueFromHeader(IHeaderAccess header) { getValueFromHeader(header.getHeader()); } /** * Get the parameter value from the option and set it into the fits header. * * @param header the header to add the parameter. * * @deprecated Use {@link #setValueInHeader(Header)} instead */ default void setValueInHeader(IHeaderAccess header) { setValueInHeader(header.getHeader()); } /** * get the value from the header and set it in the compression option. * * @param header the header of the hdu * * @throws HeaderCardException if there was a problem accessing the header */ void getValueFromHeader(Header header) throws HeaderCardException; /** * Get the parameter value from the option and set it into the fits header. * * @param header the header to add the parameter. * * @throws HeaderCardException if there was a problem accessing the header */ void setValueInHeader(Header header) throws HeaderCardException; } ICompressParameter.java000066400000000000000000000031611476377620500335000ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/apipackage nom.tam.fits.compression.provider.param.api; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** * (for internal use) Compression parameter that must be stored along the * meta data. */ public interface ICompressParameter { /** * @return the name of the parameter, normally the fits header key or column * name. */ String getName(); } ICompressParameters.java000066400000000000000000000151211476377620500336620ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/apipackage nom.tam.fits.compression.provider.param.api; import nom.tam.fits.BinaryTable; import nom.tam.fits.BinaryTableHDU; import nom.tam.fits.FitsException; import nom.tam.fits.Header; import nom.tam.fits.HeaderCardException; import nom.tam.fits.compression.algorithm.api.ICompressOption; import nom.tam.fits.compression.provider.param.base.CompressParameters; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ /** *

* (for internal use) Group of parameters that must be synchronized with the hdu meta data for a specific * compression algorithm. *

*

* NOTE, this interface is meant for internal use only. Implementing it externally to this library might not result in * the desired behavior. If you feed the need to implement compression parameters externally to what is privided by this * library, you are advised to extend the abstract {@link CompressParameters} class or of of its known subclasses * instead *

*/ public interface ICompressParameters { /** * Add the columns that hold the metadata for the parameters that are column based to the dhu. * * @param hdu the hdu to add the column * * @throws FitsException if the column could not be added. */ void addColumnsToTable(BinaryTableHDU hdu) throws FitsException; /** * create a copy of this parameter for another option (normally a copy of the current option). * * @param option the new option for the copied parameter * * @return this (builder pattern) */ ICompressParameters copy(ICompressOption option); /** * Initialize parameters for the given tile index * * @param index the 0-based tile index */ void setTileIndex(int index); /** * extract the option data from the column and set it in the option. * * @param index the index in the column. */ void getValuesFromColumn(int index); /** * extract the option values that are represented by headers from the hdu header. * * @param header the header to extract the option values. * * @deprecated Use {@link #getValuesFromHeader(Header)} instead. */ default void getValuesFromHeader(IHeaderAccess header) { getValuesFromHeader(header.getHeader()); } /** * initialize the column based options of the compression algorithm from the binary table. * * @param header the header of the hdu * @param binaryTable the table of the hdu * @param size the column size * * @throws FitsException if the column could not be initialized * * @deprecated Use {@link #initializeColumns(Header, BinaryTable, int)} instead */ default void initializeColumns(IHeaderAccess header, BinaryTable binaryTable, int size) throws FitsException { initializeColumns(header.getHeader(), binaryTable, size); } /** * extract the option values that are represented by headers from the hdu header. * * @param header the header to extract the option values. * * @throws HeaderCardException if there was an issue accessing the header */ void getValuesFromHeader(Header header) throws HeaderCardException; /** * initialize the column based options of the compression algorithm from the binary table. * * @param header the header of the hdu * @param binaryTable the table of the hdu * @param size the column size * * @throws HeaderCardException if there was an issue accessing the header * @throws FitsException if the column could not be initialized */ void initializeColumns(Header header, BinaryTable binaryTable, int size) throws HeaderCardException, FitsException; /** * initialize the column based parameter to the specified column length. * * @param length the column length. */ void initializeColumns(int length); /** * set the option values, that are column based, into the columns at the specified index. * * @param index the index in the columns to set. */ void setValuesInColumn(int index); /** * @deprecated Old, inconsistent method naming. Use {@link #setValuesInColumn(int)} instead. set the option * values, that are column based, into the columns at the specified index. * * @param index the index in the columns to set. */ @Deprecated default void setValueInColumn(int index) { setValuesInColumn(index); } /** * set the options values, that are hdu based, into the header. * * @param header the header to set the option value * * @throws HeaderCardException if the header could not be set. * * @deprecated Use {@link #setValuesInHeader(Header)} instead */ default void setValuesInHeader(IHeaderAccess header) throws HeaderCardException { setValuesInHeader(header == null ? null : header.getHeader()); } /** * set the options values, that are hdu based, into the header. * * @param header the header to set the option value * * @throws HeaderCardException if the header could not be set. */ void setValuesInHeader(Header header) throws HeaderCardException; } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/api/IHeaderAccess.java000066400000000000000000000127001476377620500324340ustar00rootroot00000000000000package nom.tam.fits.compression.provider.param.api; import nom.tam.fits.Header; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import nom.tam.fits.HeaderCard; import nom.tam.fits.HeaderCardException; import nom.tam.fits.header.IFitsHeader; /** *

* (for internal use) Interface for accessing FITS header values with runtime exceptions only. Regular header * access throws {@link HeaderCardException}s, which are hard exceptions. They really should have been softer runtime * exceptions from the start, but unfortunately that was choice this library made a very long time ago, and we therefore * stick to it, at least until the next major code revision (major version 2 at the earliest). So this class provides an * alternative access to headers converting any HeaderCardExceptions to {@link IllegalArgumentException}. *

*

* This is really just a rusty rail implementation, and rather incopmlete at it too. It has very limited support for * header access, geared very specifically towards supporting the compression classes of this library, and not mean for * use beyond. *

* * @see Header * * @deprecated This internal interface serves no purpose since 1.19. Will remove in some future. Prior to 1.19 * {@link Header} threw hard {@link HeaderCardException}, and this class was added so we can convert * these into soft {@link IllegalArgumentException} instead. However, now that we demoted * HeaderCardException to be soft exceptions itself, there is no reason to convert. It just * adds confusion. */ public interface IHeaderAccess { /** * Returns the header that this class is providing access to. * * @return the Header that we access through this class * * @since 1.19 */ Header getHeader(); /** * Sets a new integer value for the specified FITS keyword, adding it to the FITS header if necessary. * * @param key the standard or conventional FITS header keyword * @param value the integer value to assign to the keyword * * @throws IllegalArgumentException if the value could not be set as requested. * * @deprecated Just add values to the header directly */ default void addValue(IFitsHeader key, int value) throws IllegalArgumentException { getHeader().addValue(key, value); } /** * Sets a new string value for the specified FITS keyword, adding it to the FITS header if necessary. * * @param key the standard or conventional FITS header keyword * @param value the string value to assign to the keyword * * @throws IllegalArgumentException if the value could not be set as requested. * * @deprecated Just add values to the header directly */ default void addValue(IFitsHeader key, String value) throws IllegalArgumentException { getHeader().addValue(key, value); } /** * Returns the FITS header card for the given FITS keyword. It does not set a mark in the header for new additions, * making it more similar to {@link Header#getCard(IFitsHeader)}. * * @param key the standard or conventional FITS header keyword * * @return the matching FITS header card, or null if there is no such card within out grasp. * * @deprecated Use {@link Header#getCard(IFitsHeader)} instead. */ default HeaderCard findCard(IFitsHeader key) { return getHeader().findCard(key); } /** * Returns the FITS header card for the given FITS keyword. It does not set a mark in the header for new additions, * making it more similar to {@link Header#getCard(String)}. * * @param key the FITS header keyword * * @return the matching FITS header card, or null if there is no such card within out grasp. * * @deprecated Use {@link Header#getCard(String)} instead. */ default HeaderCard findCard(String key) { return getHeader().findCard(key); } } nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/api/package-info.java000066400000000000000000000031421476377620500323350ustar00rootroot00000000000000/** * (for internal use) The basics of storing compression options in FITS headers and compressed table columns. * It's weird enough that compression parameters are being handled in separate packages from the algorithms they serve, * but that's life. */ package nom.tam.fits.compression.provider.param.api; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/base/000077500000000000000000000000001476377620500273075ustar00rootroot00000000000000BundledParameters.java000066400000000000000000000117541476377620500335040ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/basepackage nom.tam.fits.compression.provider.param.base; /*- * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.util.ArrayList; import nom.tam.fits.compression.algorithm.api.ICompressOption; import nom.tam.fits.compression.provider.param.api.ICompressColumnParameter; import nom.tam.fits.compression.provider.param.api.ICompressHeaderParameter; import nom.tam.fits.compression.provider.param.api.ICompressParameters; /** * (for internal use) Compression parameters that are bundled together from distinct sets of component * parameters. For example, some tiled image compression methods will take parameters that consist of those specifically * for the compression algorithm (e.g. Rice vs HCompress) and a set of common parameters for the quantization * (floating-point to integer conversion). This class helps manage such composite parameter sets. The bundle behaves as * it it were a single set of all parameters across all its components. * * @author Attila Kovacs * * @since 1.18 */ public class BundledParameters extends CompressParameters { private ArrayList bundle; /** * Creates a new set of bundled compression parameters from the specified separate parameter components. * * @param components The components, which are to be bundled to provide a single set of parameters that span all of * them. * * @see #get(int) */ public BundledParameters(ICompressParameters... components) { bundle = new ArrayList<>(); for (ICompressParameters p : components) { if (p != null) { bundle.add(p); } } } /** * Returns the number of independent compression parameter components represented by this bundle. * * @return the number of component compression parameter sets in this bundle. * * @see #get(int) */ public int size() { return bundle.size(); } /** * Resturn the compression parameters for the specified component index. * * @param index the index of the paramete set in the bundle. * * @return the compression parameters for the particular bundle component. * * @throws IndexOutOfBoundsException if the index it negative or beyond the index of the last component. * * @see #size() */ public ICompressParameters get(int index) { return bundle.get(index); } @Override public BundledParameters copy(ICompressOption option) { throw new UnsupportedOperationException("Cannot copy parameter bundle"); } @Override protected ICompressColumnParameter[] columnParameters() { ArrayList list = new ArrayList<>(); for (ICompressParameters parms : bundle) { for (ICompressColumnParameter p : ((CompressParameters) parms).columnParameters()) { list.add(p); } } ICompressColumnParameter[] array = new ICompressColumnParameter[list.size()]; return list.toArray(array); } @Override protected ICompressHeaderParameter[] headerParameters() { ArrayList list = new ArrayList<>(); for (ICompressParameters parms : bundle) { for (ICompressHeaderParameter p : ((CompressParameters) parms).headerParameters()) { list.add(p); } } ICompressHeaderParameter[] array = new ICompressHeaderParameter[list.size()]; return list.toArray(array); } @Override public void setTileIndex(int index) { for (ICompressParameters parms : bundle) { parms.setTileIndex(index); } } } CompressColumnParameter.java000066400000000000000000000100351476377620500347040ustar00rootroot00000000000000nom-tam-fits-1.21.0/src/main/java/nom/tam/fits/compression/provider/param/basepackage nom.tam.fits.compression.provider.param.base; /* * #%L * nom.tam FITS library * %% * Copyright (C) 1996 - 2024 nom-tam-fits * %% * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * In jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. We make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. We intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * #L% */ import java.lang.reflect.Array; import nom.tam.fits.compression.provider.param.api.ICompressColumnParameter; /** *

* (for internal use) Compression parameters that are stored in the table along with the compressed data. Each * parameter is associated to a comlumn in the table, and the parameter takes the value that is stored in the same row * as the compressed data themselves. *

*

* It is possible to make independent copies of a set of such parameters, e.g. for parallel processing. In such cases * all copies share their underlying column data with the original, so changing the sotrage array of column data in * either the original or any of its decendants will affect the original and all decendans equally. *

* * @author Attila Kovacs * * @param The generic array type that contains the individual parameters for each tile as a table column. * @param