cbz_tools 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7fe3f88fb29727f7962453a2fee499507a67111de66b535e4306dd88a569c19
4
- data.tar.gz: 5513eadeb2da736150f1423aff4d4b37ae7229946bcee1e1fbe599894c639295
3
+ metadata.gz: 34014f96dd63882bd66f0de3f3a3548faa507e279d37f335fc12ecf8cef3ab41
4
+ data.tar.gz: f54439b98ddad12995b30e74ca1d6e45f559acc5c7df0190af5610623e1911e1
5
5
  SHA512:
6
- metadata.gz: c424857bf97688fb045f1e2f26b7740dd85fc6a8cd38e6cce6acad5c9d3f2a9900b510843b03dc292aaa4873dc68f45fb11ada59730c648988a8eb54d0c10841
7
- data.tar.gz: b486e668e9106525f2053d8ac2e36f9271717ee524b934009f1f8f752eb3be007a1de09c281dc1afc36305382de81917d16e280b5dd94aa0d9644f48656252bc
6
+ metadata.gz: 37bc01a7b250c5382507314c57d6c3c208f122c8fd39802ce5308aec16baa07860530e120f37cb5ce69bbf5dcdcc0a145e7a3ce0ad81fc2276ba4ae0e461c835
7
+ data.tar.gz: 9b83a50b99751749896e4d395af34665d85004a49768b7d7b9dac9d578bf4d90fd58190e87c18415620ae3935def84893a61ed40c7dc93ccc682ca31cd819fd5
@@ -0,0 +1,3 @@
1
+ {
2
+ "cSpell.words": ["comicinfo", "gtin", "penciller"]
3
+ }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-08-03
4
+
5
+ - Add `CbzTools::ComicInfo` (parse/build ComicInfo.xml, `SCHEMA`); raises `ParseError` on bad XML.
6
+ - **Breaking**: `Reader#comic_info` returns parsed hash (`{}` when absent), not raw XML.
7
+ - Add `Reader#update_comic_info(metadata)` to write `ComicInfo.xml` back.
8
+ - `Reader#read` is now private; use `first_image`, `image`, `comic_info`.
9
+
10
+ ## [0.2.0] - 2026-08-03
11
+
12
+ - Add `CbzTools::Reader` for reading CBZ archives (images and `ComicInfo.xml`)
13
+
3
14
  ## [0.1.1] - 2026-08-03
4
15
 
5
16
  - Exclude development-only files from the packaged gem
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module CbzTools
6
+ # Reads and writes the ComicInfo.xml metadata embedded in CBZ archives.
7
+ #
8
+ # The two operations are symmetric over SCHEMA: every key on the left
9
+ # side of that hash is a field that {.parse} can read and {.build} can
10
+ # write. Extend the schema in place (for example, in an initializer)
11
+ # if you need to support custom fields.
12
+ module ComicInfo
13
+ class ParseError < Error
14
+ end
15
+
16
+ SCHEMA = {
17
+ title: "Title",
18
+ series: "Series",
19
+ series_sort: "SeriesSort",
20
+ localized_series: "LocalizedSeries",
21
+ number: "Number",
22
+ count: "Count",
23
+ volume: "Volume",
24
+ summary: "Summary",
25
+ notes: "Notes",
26
+ publisher: "Publisher",
27
+ imprint: "Imprint",
28
+ year: "Year",
29
+ month: "Month",
30
+ day: "Day",
31
+ writer: "Writer",
32
+ penciller: "Penciller",
33
+ inker: "Inker",
34
+ colorist: "Colorist",
35
+ letterer: "Letterer",
36
+ cover_artist: "CoverArtist",
37
+ editor: "Editor",
38
+ translator: "Translator",
39
+ genre: "Genre",
40
+ tags: "Tags",
41
+ web: "Web",
42
+ page_count: "PageCount",
43
+ language_iso: "LanguageISO",
44
+ format: "Format",
45
+ series_group: "SeriesGroup",
46
+ age_rating: "AgeRating",
47
+ community_rating: "CommunityRating",
48
+ black_and_white: "BlackAndWhite",
49
+ manga: "Manga",
50
+ characters: "Characters",
51
+ teams: "Teams",
52
+ locations: "Locations",
53
+ scan_information: "ScanInformation",
54
+ story_arc: "StoryArc",
55
+ gtin: "GTIN"
56
+ }
57
+
58
+ # Parses a ComicInfo.xml string into a symbol-keyed hash.
59
+ #
60
+ # Returns an empty hash when the input is empty, or when the XML is
61
+ # valid but has no <ComicInfo> root. Keys not in SCHEMA are ignored.
62
+ # Fields whose text is empty or whitespace-only are dropped.
63
+ #
64
+ # @param xml [String] the ComicInfo.xml document
65
+ # @return [Hash] parsed metadata, keyed by SCHEMA symbols
66
+ # @raise [ParseError] when the XML is malformed
67
+ def self.parse(xml)
68
+ doc = Nokogiri::XML(xml)
69
+ raise ParseError, "Malformed XML: #{doc.errors.first&.message}" if doc.errors.any?
70
+
71
+ root = doc.at("ComicInfo")
72
+ return {} unless root
73
+
74
+ SCHEMA.each_with_object({}) do |(key, tag), hash|
75
+ text = root.at(tag)&.text&.strip
76
+ hash[key] = text if text && !text.empty?
77
+ end
78
+ end
79
+
80
+ # Builds a ComicInfo.xml string from a metadata hash.
81
+ #
82
+ # Accepts symbol or string keys; non-SCHEMA keys are ignored. Values
83
+ # that are +nil+, empty, or whitespace-only are skipped so the
84
+ # resulting XML does not contain empty tags.
85
+ #
86
+ # @param metadata [Hash] field values keyed by SCHEMA symbols or strings
87
+ # @return [String] a ComicInfo.xml document
88
+ def self.build(metadata)
89
+ normalized = metadata.transform_keys(&:to_sym)
90
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
91
+ xml.ComicInfo(
92
+ "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
93
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
94
+ ) do
95
+ SCHEMA.each do |key, tag|
96
+ value = normalized[key]
97
+ next if value.nil? || value.to_s.strip.empty?
98
+
99
+ xml.send(tag, value.to_s)
100
+ end
101
+ end
102
+ end
103
+ builder.to_xml
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CbzTools
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zip"
4
+
5
+ require_relative "comic_info"
6
+
7
+ module CbzTools
8
+ # Reads the contents of a CBZ (Comic Book ZIP) archive.
9
+ #
10
+ # The public surface is image access and parsed ComicInfo metadata;
11
+ # XML is an internal wire format and never appears on the API.
12
+ #
13
+ # @example Reading images and metadata
14
+ # CbzTools::Reader.open("path/to/comic.cbz") do |reader|
15
+ # reader.image_names.first # => "page_001.jpg"
16
+ # reader.first_image # => "<image bytes>"
17
+ # reader.comic_info # => {title: "The Amazing Spider-Man", page_count: "32"}
18
+ # end
19
+ class Reader
20
+ # File extensions recognized as comic page images.
21
+ IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp].freeze
22
+
23
+ # Raised when {#image} is called with a name that does not match
24
+ # any entry in the archive.
25
+ class ImageNotFoundError < Error; end
26
+
27
+ # Opens a CBZ archive, yields a reader, and closes it when the
28
+ # block exits — including on exception.
29
+ #
30
+ # @yieldparam reader [Reader] the open reader
31
+ # @return [void]
32
+ def self.open(path, &block)
33
+ reader = new(path)
34
+ block.call(reader)
35
+ ensure
36
+ reader&.close
37
+ end
38
+
39
+ # Opens the archive at the given path. Prefer {.open} unless you
40
+ # need to hold the reader open across multiple operations.
41
+ #
42
+ # @param path [String] path to a CBZ file
43
+ def initialize(path)
44
+ @path = path
45
+ @zip = Zip::File.open(@path)
46
+ end
47
+
48
+ # Releases the underlying zip handle. Safe to call more than once.
49
+ #
50
+ # @return [void]
51
+ def close
52
+ @zip&.close
53
+ @zip = nil
54
+ end
55
+
56
+ # Returns the names of all image entries in the archive, sorted.
57
+ #
58
+ # @return [Array<String>]
59
+ def image_names
60
+ @zip.entries
61
+ .reject(&:directory?)
62
+ .map(&:name)
63
+ .select { |name| !name.include?("__macosx") && image?(name) }
64
+ .sort
65
+ end
66
+
67
+ # Returns the bytes of the first image in the archive, or +nil+
68
+ # when there are no images.
69
+ #
70
+ # @return [String, nil]
71
+ def first_image
72
+ read(image_names.first)
73
+ end
74
+
75
+ # Returns the bytes of a specific image by name.
76
+ #
77
+ # @param name [String] entry name within the archive
78
+ # @return [String]
79
+ # @raise [ImageNotFoundError] when no entry with that name exists
80
+ def image(name)
81
+ data = read(name)
82
+ raise ImageNotFoundError, "Image '#{name}' not found in #{@path}" unless data
83
+
84
+ data
85
+ end
86
+
87
+ # Returns the parsed ComicInfo metadata as a symbol-keyed hash.
88
+ #
89
+ # Returns an empty hash when the archive has no ComicInfo.xml.
90
+ # The lookup is case-insensitive (+comicinfo.xml+ is also
91
+ # recognized).
92
+ #
93
+ # @return [Hash] parsed metadata, keyed by SCHEMA symbols
94
+ # @raise [CbzTools::ComicInfo::ParseError] when ComicInfo.xml is malformed
95
+ def comic_info
96
+ xml = read("ComicInfo.xml") || read("comicinfo.xml")
97
+ xml ? CbzTools::ComicInfo.parse(xml) : {}
98
+ end
99
+
100
+ # Writes the given metadata hash back into the archive as
101
+ # ComicInfo.xml, replacing any existing entry with that name.
102
+ #
103
+ # Accepts symbol or string keys; non-SCHEMA keys are ignored.
104
+ #
105
+ # @param metadata [Hash] field values keyed by SCHEMA symbols or strings
106
+ # @return [void]
107
+ # @raise [Zip::Error] when the archive cannot be opened for writing
108
+ # @raise [SystemCallError] when an underlying I/O operation fails
109
+ def update_comic_info(metadata)
110
+ @zip.get_output_stream("ComicInfo.xml") do |f|
111
+ f.write(CbzTools::ComicInfo.build(metadata))
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def read(name)
118
+ entry = @zip.find_entry(name)
119
+ return nil unless entry
120
+
121
+ entry.get_input_stream.read
122
+ end
123
+
124
+ def image?(name)
125
+ IMAGE_EXTENSIONS.any? { |ext| name.downcase.end_with?(ext) }
126
+ end
127
+ end
128
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CbzTools
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/cbz_tools.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "cbz_tools/version"
4
-
5
- module CbzTools
6
- class Error < StandardError; end
7
- # Your code goes here...
8
- end
4
+ require_relative "cbz_tools/error"
5
+ require_relative "cbz_tools/reader"
6
+ require_relative "cbz_tools/comic_info"
data/sig/cbz_tools.rbs CHANGED
@@ -1,4 +1,48 @@
1
1
  module CbzTools
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class Reader
8
+ IMAGE_EXTENSIONS: Array[String]
9
+
10
+ class ImageNotFoundError < CbzTools::Error
11
+ end
12
+
13
+ def self.open: (String path) { (Reader reader) -> void } -> void
14
+
15
+ def initialize: (String path) -> void
16
+
17
+ def close: -> void
18
+
19
+ def image_names: -> Array[String]
20
+
21
+ def read: (String name) -> String?
22
+
23
+ def first_image: -> String?
24
+
25
+ def image: (String name) -> String
26
+
27
+ def comic_info: -> Hash[Symbol, String]
28
+
29
+ def update_comic_info: (Hash[Symbol | String, String] metadata) -> void
30
+
31
+ private
32
+
33
+ def read: (String name) -> String?
34
+
35
+ def image?: (String name) -> bool
36
+ end
37
+
38
+ module ComicInfo
39
+ SCHEMA: Hash[Symbol, String]
40
+
41
+ class ParseError < CbzTools::Error
42
+ end
43
+
44
+ def self.parse: (String xml) -> Hash[Symbol, String]
45
+
46
+ def self.build: (Hash[Symbol | String, String] metadata) -> String
47
+ end
4
48
  end
metadata CHANGED
@@ -1,14 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cbz_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Figueroa
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rubyzip
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: nokogiri
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.15'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.15'
12
40
  description: 'Tools for working with CBZ comic book archives: reading and extracting
13
41
  their contents, reading and updating their embedded metadata, and converting CBR
14
42
  archives to CBZ.'
@@ -18,10 +46,14 @@ executables: []
18
46
  extensions: []
19
47
  extra_rdoc_files: []
20
48
  files:
49
+ - ".vscode/settings.json"
21
50
  - CHANGELOG.md
22
51
  - LICENSE.txt
23
52
  - README.md
24
53
  - lib/cbz_tools.rb
54
+ - lib/cbz_tools/comic_info.rb
55
+ - lib/cbz_tools/error.rb
56
+ - lib/cbz_tools/reader.rb
25
57
  - lib/cbz_tools/version.rb
26
58
  - sig/cbz_tools.rbs
27
59
  homepage: https://github.com/dmfigueroa/cbz_tools