lower level text-handler

本文详细介绍了iOS应用开发中使用的多种文本处理技术,包括基础文本绘制、低级文本显示类、Core Text技术、简单文本输入、文本位置与范围管理、以及与文本输入系统通信等关键概念。通过实例代码,展示了如何使用这些技术实现文本的高效绘制、布局、输入和编辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Lower Level Text-Handling Technologies

Most apps can use the high-level text display classes and Text Kit for all their text handling. You might have an app, however, that requires the lower level programmatic interfaces from the Core Text, Core Graphics, and Core Animation frameworks as well as other APIs in UIKit itself.

Simple Text Drawing

In addition to the UIKit classes for displaying and editing text, iOS also includes several ways to draw text directly on the screen. The easiest and most efficient way to draw simple strings is using the UIKit additions to the NSString class, which is in a category named UIStringDrawing. These extensions include methods for drawing strings using a variety of attributes wherever you want them on the screen. There are also methods for computing the size of a rendered string before you actually draw it, which can help you lay out your app content more precisely.

Important: There are some good reasons to avoid drawing text directly in favor of using the text objects of the UIKit framework. One is performance. Although, a UILabelobject also draws its static text, it draws it only once whereas a text-drawing routine is typically called repeatedly. Text objects also afford more interaction; for example, they are selectable.

The methods of UIStringDrawing draw strings at a given point (for single lines of text) or within a specified rectangle (for multiple lines). You can pass in attributes used in drawing—for example, font, line-break mode, and baseline adjustment. The methods of UIStringDrawing allow you to adjust the position of the rendered text precisely and blend it with the rest of your view’s content. They also let you compute the bounding rectangle for your text in advance based on the desired font and style attributes.

You can also use the CATextLayer class of Core Animation to do simple text drawing. An object of this class stores a plain string or attributed string as its content and offers a set of attributes that affect that content, such as font, font size, text color, and truncation behavior. The advantage of CATextLayer is that (being a subclass of CALayer) itsproperties are inherently capable of animation. Core Animation is associated with the QuartzCore framework. Because instances of CATextLayer know how to draw themselves in the current graphics context, you don’t need to issue any explicit drawing commands when using those instances.

For information about the string-drawing extensions to NSString, see NSString UIKit Additions Reference. To learn more about CATextLayerCALayer, and the other classes of Core Animation, read Core Animation Programming Guide.

Core Text

Core Text is a technology for custom text layout and font management. App developers typically have no need to use Core Text directly. Text Kit is built on top of Core Text, giving it the same advantages, such as speed and sophisticated typographic capability. In addition, Text Kit provides a great deal of infrastructure that you must build for yourself if you use Core Text.

However, the Core Text API is accessible to developers who must use it directly. It is intended to be used by apps that have their own layout engines—for example, a word processor that has its own page layout engine can use Core Text to generate the glyphs and position them relative to each other.

Core Text is implemented as a framework that publishes an API similar to that of Core Foundation—similar in that it is procedural (ANSI C) but is based on object-like opaque types. This API is integrated with both Core Foundation and Core Graphics. For example, Core Text uses Core Foundation and Core Graphics objects in many input and output parameters. Moreover, because many Core Foundation objects are “toll-free bridged” with their counterparts in the Foundation framework, you may use some Foundation objects in the parameters of Core Text functions.

Note: If you use Core Text or Core Graphics to draw text, remember that you must apply a flip transform to the current text matrix to have text displayed in its proper orientation—that is, with the drawing origin at the upper-left corner of the string’s bounding box.

Core Text has two major parts: a layout engine and font technology, each backed by its own collection of opaque types.

Core Text Layout Opaque Types

Core Text requires two objects whose opaque types are not native to it: an attributed string (CFAttributedStringRef) and a graphics path (CGPathRef). An attributed-string object encapsulates a string backing the displayed text and includes properties (or, “attributes”) that define stylistic aspects of the characters in the string—for example, font and color. The graphics path defines the shape of a frame of text, which is equivalent to a paragraph.

Core Text objects at runtime form a hierarchy that is reflective of the level of the text being processed (see Figure 10-1). At the top of this hierarchy is the framesetter object (CTFramesetterRef). With an attributed string and a graphics path as input, a framesetter generates one or more frames of text (CTFrameRef). As the text is laid out in a frame, the framesetter applies paragraph styles to it, including such attributes as alignment, tab stops, line spacing, indentation, and line-breaking mode.

To generate frames, the framesetter calls a typesetter object (CTTypesetterRef). The typesetter converts the characters in the attributed string to glyphs and fits those glyphs into the lines that fill a text frame. (A glyph is a graphic shape used to represent a character.) A line in a frame is represented by a CTLine object (CTLineRef). A CTFrameobject contains an array of CTLine objects.

CTLine object, in turn, contains an array of glyph runs, represented by objects of the CTRunRef type. A glyph run is a series of consecutive glyphs that have the same attributes and direction. Although a typesetter object returns CTLine objects, it composes those lines from arrays of glyph runs.

Figure 10-1  Core Text layout objects

Using functions of the CTLine opaque type, you can draw a line of text from an attributed string without having to go through the CTFramesetter object. You simply position the origin of the text on the text baseline and request the line object to draw itself.

Core Text Font Opaque Types

Fonts are essential to text processing in Core Text. The typesetter object uses fonts (along with the source attributed string) to convert glyphs from characters and then position those glyphs relative to one another. A graphics context is central to fonts in Core Text. You can use graphics-context functions to set the current font and draw glyphs; or you can create a CTLine object from an attributed string and use its functions to draw into the graphics context. The Core Text font system handles Unicode fonts natively.

The font system includes objects of three opaque types: CTFontCTFontDescriptor, and CTFontCollection:

  • Font objects (CTFontRef) are initialized with a point size and specific characteristics (from a transformation matrix). You can query the font object for its character-to-glyph mapping, its encoding, glyph data, and metrics such as ascent, leading, and so on. Core Text also offers an automatic font-substitution mechanism called font cascading.

  • Font descriptor objects (CTFontDescriptorRef) are typically used to create font objects. Instead of dealing with a complex transformation matrix, they allow you to specify a dictionary of font attributes that include such properties as PostScript name, font family and style, and traits (for example, bold or italic).

  • Font collection objects (CTFontCollectionRef) are groups of font descriptors that provide services such as font enumeration and access to global and custom font collections.

It’s possible to convert UIFont objects to CTFont objects by calling CTFontCreateWithName, passing the font name and point size encapsulated by the UIFont object.

Core Graphics Text Drawing

Core Graphics (or Quartz) is the system framework that handles two-dimensional imaging at the lowest level. Text drawing is one of its capabilities. Generally, because Core Graphics is so low-level, it is recommended that you use one of the system’s other technologies for drawing text. However, if circumstances require it, you can draw text with Core Graphics.

You select fonts, set text attributes, and draw text using functions of the CGContext opaque type. For example, you can call CGContextSelectFont to set the font used, and then call CGContextSetFillColor to set the text color. You then set the text matrix (CGContextSetTextMatrix) and draw the text using CGContextShowGlyphsAtPoint.

For more information about these functions and their use, see Quartz 2D Programming Guide and Core Graphics Framework Reference.

Foundation-Level Regular Expressions

The NSString class of the Foundation framework includes a simple programmatic interface for regular expressions. You call one of three methods that return a range, passing in a specific option constant and a regular-expression string. If there is a match, the method returns the range of the substring. The option is theNSRegularExpressionSearch constant, which is of bit-mask type NSStringCompareOptions; this constant tells the method to expect a regular-expression pattern rather than a literal string as the search value. The supported regular expression syntax is that defined by ICU (International Components for Unicode).

Note: In addition to the NSString regular-expression feature described here, iOS provides more complete support for regular expressions with the NSRegularExpressionclass. The ICU User Guide describes how to construct ICU regular expressions (http://userguide.icu-project.org/strings/regexp).

The NSString methods for regular expressions are the following:

If you specify the NSRegularExpressionSearch option in these methods, the only other NSStringCompareOptions options you may specify are NSCaseInsensitiveSearchand NSAnchoredSearch. If a regular-expression search does not find a match or the regular-expression syntax is malformed, these methods return an NSRange structure with a value of {NSNotFound, 0}.

Listing 10-1 gives an example of using the NSString regular-expression API.

Listing 10-1  Finding a substring using a regular expression

    // finds phone number in format nnn-nnn-nnnn
    NSRange r;
    NSString *regEx = @"[0-9]{3}-[0-9]{3}-[0-9]{4}";
    r = [textView.text rangeOfString:regEx options:NSRegularExpressionSearch];
    if (r.location != NSNotFound) {
        NSLog(@"Phone number is %@", [textView.text substringWithRange:r]);
    } else {
        NSLog(@"Not found.");
    }

Because these methods return a single range value for the substring matching the pattern, certain regular-expression capabilities of the ICU library are either not available or have to be programmatically added. In addition, NSStringCompareOptions options such as backward search, numeric search, and diacritic-insensitive search are not available and capture groups are not supported.

When testing the returned range, you should be aware of certain behavioral differences between searches based on literal strings and searches based on regular-expression patterns. Some patterns can successfully match and return an NSRange structure with a length of 0 (in which case the location field is of interest). Other patterns can successfully match against an empty string or, in those methods with a range parameter, with a zero-length search range.

ICU Regular-Expression Support

In case the NSString support for regular expressions is not sufficient for your needs, a modified version of the libraries from ICU 4.2.1 is included in iOS at the BSD (nonframework) level of the system. ICU (International Components for Unicode) is an open-source project for Unicode support and software internationalization. The installed version of ICU includes those header files necessary to support regular expressions, along with some modifications related to those interfaces, namely:

  • parseerr.h

  • platform.h

  • putil.h

  • uconfig.h

  • udraft.h

  • uintrnal.h

  • uiter.h

  • umachine.h

  • uregex.h

  • urename.h

  • ustring.h

  • utf_old.h

  • utf.h

  • utf16.h

  • utf8.h

  • utypes.h

  • uversion.h

You can read the ICU 4.2 API documentation and user guide at http://icu-project.org/apiref/icu4c/index.html.

Simple Text Input

An app that wants to display and process text is not limited to the text and web objects of the UIKit framework. It can implement custom views that are capable of anything from simple text entry to complex text processing and custom input. Through the available programming interfaces, these apps can acquire features such as custom text layout, multistage input, autocorrection, custom keyboards, and spell-checking.

You can implement custom views that allow users to enter text at an insertion point and delete characters before that insertion point when they tap the Delete key. An instant-messaging app, for example, could have a view that allows users to enter their part of a conversation.

You can acquire this capability for simple text entry by subclassing UIView, or any other view class that inherits from UIResponder, and adopting the UIKeyInput protocol. When an instance of your view class becomes the first responder, UIKit displays the system keyboard. UIKeyInput itself adopts the UITextInputTraits protocol, so you can set keyboard type, return-key type, and other attributes of the keyboard.

Note: Only a subset of the available keyboards and languages are available to classes that adopt only the UIKeyInput protocol. For example, any multi-stage input method, such Chinese, Japanese, Korean, and Thai is excluded. If a class also adopts the UITextInput protocol, those input methods are then available.

To adopt UIKeyInput, you must implement the three methods it declares: hasTextinsertText:, and deleteBackward. To do the actual drawing of the text, you may use any of the technologies summarized in this chapter. However, for simple text input, such as for a single line of text in a custom control, the UIStringDrawing andCATextLayer APIs are most appropriate.

Listing 10-2 illustrates the UIKeyInput implementation of a custom view class. The textStore property in this example is an NSMutableString object that serves as the backing store of text. The implementation either appends or removes the last character in the string (depending on whether an alphanumeric key or the Delete key is pressed) and then redraws textStore.

Listing 10-2  Implementing simple text entry

- (BOOL)hasText {
    if (textStore.length > 0) {
        return YES;
    }
    return NO;
}
 
- (void)insertText:(NSString *)theText {
    [self.textStore appendString:theText];
    [self setNeedsDisplay];
}
 
- (void)deleteBackward {
    NSRange theRange = NSMakeRange(self.textStore.length-1, 1);
    [self.textStore deleteCharactersInRange:theRange];
    [self setNeedsDisplay];
}
 
- (void)drawRect:(CGRect)rect {
    CGRect rectForText = [self rectForTextWithInset:2.0]; // custom method
    [self.theColor set];
    UIRectFrame(rect);
    [self.textStore drawInRect:rectForText withFont:self.theFont];
}

To actually draw the text in the view, this code uses the drawInRect:withFont: from the UIStringDrawing category on NSString.

Communicating with the Text Input System

The text input system of iOS manages the keyboard. It interprets taps as presses of specific keys in specific keyboards suitable for certain languages. It then sends the associated character to the target view for insertion. As explained in Simple Text Input, view classes must adopt the UIKeyInputprotocol to insert and delete characters at the caret (insertion point).

However, the text input system does more than simple text entry. For example, it manages autocorrection and multistage input, which are all based upon the current selection and context. Multistage text input is required for ideographic languages such as Kanji (Japanese) and Hanzi (Chinese), which take input from phonetic keyboards. To acquire these features, a custom text view must communicate with the text input system by adopting the UITextInput protocol and implementing the related client-side classes and protocols.

The following section describes the general responsibilities of a custom text view that communicates with the text input system. A Guided Tour of a UITextInput Implementation examines the most important classes and methods of a typical implementation of UITextInput.

Overview of the Client Side of Text Input

A class that wants to communicate with the text input system must adopt the UITextInput protocol. The class needs to inherit from UIResponder and is in most cases a custom view.

Note: The responder class that adopts UITextInput does not have to be the view that draws and manages text (as is the case in the sample code analyzed in A Guided Tour of a UITextInput Implementation). However, if it isn’t the view that draws and manages text, the class that does adopt UITextInput should be able to communicate directly with the view that does. For simplicity’s sake, the following discussion refers to the responder class that adopts UITextInput as the text view.

The text view must do its own text layout and font management; for this purpose, the Core Text framework is recommended. (Core Text gives an overview of Core Text.) The class should also adopt and implement the UIKeyInput protocol and should set the necessary properties of the UITextInputTraits protocol.

The general architecture of the client and system sides of the text input system are diagrammed in Figure 10-2.

Figure 10-2  Paths of communication with the text input system

The text input system calls the UITextInput methods that the text view implements. Many of these methods request information about specific text positions and text ranges from the text view and pass the same information back to the class in other method calls. The reasons for these exchanges of text positions and text ranges are summarized in Tasks of a UITextInput Object.

Text positions and text ranges in the text input system are represented by instances of custom classes. Text Positions and Text Ranges discusses these objects in more detail.

The text view also maintains references to a tokenizer and an input delegate. The text view calls methods declared by the UITextInputDelegate protocol to notify a system-provided input delegate about external changes in text and selection. The text input system communicates with a tokenizer object to determine the granularity of text units—for example, character, word, and paragraph. The tokenizer is an object that adopts the UITextInputTokenizer protocol. The text view includes a property (declared byUITextInput) that holds a reference to the tokenizer.

Text Positions and Text Ranges

The client app must create two classes whose instances represent positions and ranges of text in a text view. These classes must be subclasses of UITextPosition andUITextRange.

Although UITextPosition itself declares no methods or properties, it is an essential part of the information exchanged between a text document and the text input system. The text input system requires an object to represent a location in the text instead of, say, an integer or a structure. Moreover, a UITextPosition object can serve a practical purpose by representing a position in the visible text when the string backing the text has a different offset to that position. This happens when the string contains invisible formatting characters, such as with RTF and HTML documents, or embedded objects, such as an attachment. The custom UITextPosition class can account for these invisible characters when locating the string offsets of visible characters. In the simplest case—a plain text document with no embedded objects—a custom UITextPositionobject can encapsulate a single offset or index integer.

UITextRange declares a simple interface in which two of its properties are starting and ending custom UITextPosition objects. The third property holds a Boolean value that indicates whether the range is empty (that is, has no length).

Tasks of a UITextInput Object

A class adopting the UITextInput protocol is required to implement most of the protocol’s methods and properties. With a few exceptions, these methods take customUITextPosition or UITextRange objects as parameters or return one of these objects. At runtime the text system invokes these methods and, again in almost all cases, expects some object or value back.

The text view must assign text positions to properties marking the beginning and end of the displayed text. In addition, it must also maintain the range of the currently selected text and the range of the currently marked text, if any. Marked text, which is part of multistage text input, represents provisionally inserted text the user has yet to confirm. It is styled in a distinctive way. The range of marked text always contains within it a range of selected text, which might be a range of characters or the caret.

The methods implemented by a UITextInput object can be divided into distinctive tasks:

The UITextInput object might also choose to implement one or more optional protocol methods. These enable it to return text styles (font, text color, background color) beginning at a specified text position and to reconcile visible text position and character offset (for those UITextPosition objects where these values are not the same).

When changes occur in the text view due to external reasons—that is, they aren't caused by calls from the text input system—the UITextInput object should sendtextWillChange:textDidChange:selectionWillChange:, and selectionDidChange: messages to the input delegate (which it holds a reference to). For example, when users tap a text view and you set the range of selected text to place the insertion point under the finger, you would send selectionWillChange: before you change the selected range, and you send selectionDidChange: after you change the range.

Tokenizers

Tokenizers are objects that determine whether a text position is within or at the boundary of a text unit with a given granularity. When queried by the text input system, a tokenizer returns ranges of text units with a given granularity or the boundary text position for a text unit with a given granularity. Currently defined granularities are character, word, sentence, paragraph, line, and document; enum constants of the UITextGranularity type represent these granularities. Granularities of text units are always evaluated with reference to a storage or layout direction.

The text input system uses the tokenizer in a variety of ways. For example, the keyboard might require the last sentence’s worth of context to figure out what the user is trying to type. Or, if the user is pressing the Option-left arrow key (on an external keyboard), the text system queries the tokenizer to find the information it needs to move to the previous word.

A tokenizer is an instance of a class that conforms to the UITextInputTokenizer protocol. The UITextInputStringTokenizer class provides a default base implementation of the UITextInputTokenizer protocol that is suitable for all supported languages. If you require a tokenizer with an entirely new interpretation of text units of varying granularity, you should adopt UITextInputTokenizer and implement all of its methods. Otherwise you should subclass UITextInputStringTokenizer to provide app-specific information about layout directions.

When you initialize a UITextInputStringTokenizer object, you supply it with the view adopting the UITextInput protocol. In turn, the UITextInput object should lazily create its tokenizer object in the getter method of the tokenizer property.

A Guided Tour of a UITextInput Implementation

SimpleTextInput is a simple text-editing app based on Core Text. It has two custom subclasses of UIView. One view subclass, SimpleCoreTextView, provides text layout and editing support using Core Text. The other view subclass, EditableCoreTextView, adopts the UIKeyInput protocol to enable text input; it also adopts the UITextInputprotocol and creates and implements the related subclasses to communicate with the text input system. EditableCoreTextView embeds SimpleCoreTextView as an instance variable, instantiates it, and calls through to it in most UITextInput and UIKeyInput method implementations.

Note: For reasons of space, the guided tour shows implementations of the UITextInput methods that are most important or illustrative. However, it is possible to extrapolate from these chosen implementations to the others of the protocols. The code was taken from the SimpleTextInput sample code project.

Subclasses of UITextPosition and UITextRange

EditableCoreTextView creates a custom subclass of UITextPosition called IndexedPosition and a custom subclass of UITextRange called IndexedRange. These subclasses simply encapsulate a single index value and an NSRange value based on two of those indexes. Listing 10-3 shows the declaration of these classes.

Listing 10-3  Declaring the IndexedPosition and IndexedRange classes

@interface IndexedPosition : UITextPosition {
    NSUInteger _index;
    id <UITextInputDelegate> _inputDelegate;
}
@property (nonatomic) NSUInteger index;
+ (IndexedPosition *)positionWithIndex:(NSUInteger)index;
@end
 
@interface IndexedRange : UITextRange {
    NSRange _range;
}
@property (nonatomic) NSRange range;
+ (IndexedRange *)rangeWithNSRange:(NSRange)range;
 
@end

Both classes declare class factory methods to vend instances. Listing 10-4 shows the implementation of these methods as well as the methods declared by the UITextRangeclass.

Listing 10-4  Implementing the IndexedPosition and IndexedRange classes

@implementation IndexedPosition
@synthesize index = _index;
 
+ (IndexedPosition *)positionWithIndex:(NSUInteger)index {
    IndexedPosition *pos = [[IndexedPosition alloc] init];
    pos.index = index;
    return [pos autorelease];
}
 
@end
 
@implementation IndexedRange
@synthesize range = _range;
 
+ (IndexedRange *)rangeWithNSRange:(NSRange)nsrange {
    if (nsrange.location == NSNotFound)
        return nil;
    IndexedRange *range = [[IndexedRange alloc] init];
    range.range = nsrange;
    return [range autorelease];
}
 
- (UITextPosition *)start {
    return [IndexedPosition positionWithIndex:self.range.location];
}
 
- (UITextPosition *)end {
        return [IndexedPosition positionWithIndex:(self.range.location + self.range.length)];
}
 
-(BOOL)isEmpty {
    return (self.range.length == 0);
}
@end
Inserting and Deleting Text

A text view that adopts the UITextInput protocol must also adopt the UIKeyInput protocol. That means it must implement the insertText:deleteBackward, andhasText methods as discussed in Simple Text Input. Because the EditableCoreTextView class is adopting UITextInput, it must also maintain the selected and marked text ranges (that is, the current values of the selectedTextRange and markedTextRange properties) as text is entered and deleted.

Listing 10-5 illustrates how EditableCoreTextView does this when text is entered. If there is marked text when a character is entered, it replaces the marked text with the character by calling the replaceCharactersInRange:withString: method on the backing mutable string. If there is a selected range of text, it replaces the characters in that range with the input character. Otherwise, the method inserts the input character at the caret.

Listing 10-5  Inserting text input into storage and updating selected and marked ranges

- (void)insertText:(NSString *)text {
    NSRange selectedNSRange = _textView.selectedTextRange;
    NSRange markedTextRange = _textView.markedTextRange;
 
    if (markedTextRange.location != NSNotFound) {
        [_text replaceCharactersInRange:markedTextRange withString:text];
        selectedNSRange.location = markedTextRange.location + text.length;
        selectedNSRange.length = 0;
        markedTextRange = NSMakeRange(NSNotFound, 0);
    } else if (selectedNSRange.length > 0) {
        [_text replaceCharactersInRange:selectedNSRange withString:text];
        selectedNSRange.length = 0;
        selectedNSRange.location += text.length;
    } else {
        [_text insertString:text atIndex:selectedNSRange.location];
        selectedNSRange.location += text.length;
    }
    _textView.text = _text;
    _textView.markedTextRange = markedTextRange;
    _textView.selectedTextRange = selectedNSRange;
}

Even though the structure of the deleteBackward method implemented by EditableCoreTextView is identical to the insertText: method, there are appropriate differences in how the selected and marked text ranges are adjusted. Another difference is that the deleteCharactersInRange: method is called on the backing mutable string rather than replaceCharactersInRange:withString:.

Returning and Replacing Text by Range

Any text view that communicates with the text input system must, when requested, return a specified range of text and replace a range of text with a given string. The classes in our example, EditableCoreTextView and SimpleCoreTextView, maintain synchronized copies of the backing string object (EditableCoreTextView as aNSMutableString object). The implementations of textInRange: and replaceRange:withText: in Listing 10-6 call the appropriate NSString methods on the backing string to accomplish their essential functions.

Listing 10-6  Implementations of textInRange: and replaceRange:withText:

- (NSString *)textInRange:(UITextRange *)range
{
    IndexedRange *r = (IndexedRange *)range;
    return ([_text substringWithRange:r.range]);
}
 
- (void)replaceRange:(UITextRange *)range withText:(NSString *)text
{
    IndexedRange *r = (IndexedRange *)range;
    NSRange selectedNSRange = _textView.selectedTextRange;
    if ((r.range.location + r.range.length) <= selectedNSRange.location) {
        selectedNSRange.location -= (r.range.length - text.length);
    } else {
        // Need to also deal with overlapping ranges.
    }
    [_text replaceCharactersInRange:r.range withString:text];
    _textView.text = _text;
    _textView.selectedTextRange = selectedNSRange;
}

When the text property of SimpleCoreTextView changes (as shown in the implementation of replaceRange:withText:), SimpleCoreTextView lays out the text again and redraws it using Core Text functions.

Maintaining Selected and Marked Text Ranges

Because editing operations are performed on selected and marked text, the text input system frequently requests that the text view return and set the ranges of selected and marked text. Listing 10-7 shows how EditableCoreTextView returns the ranges of selected and marked text by implementing getter methods for the selectedTextRangeand markedTextRangeproperties.

Listing 10-7  Returning ranges of selected and marked text

- (UITextRange *)selectedTextRange {
    return [IndexedRange rangeWithNSRange:_textView.selectedTextRange];
}
 
- (UITextRange *)markedTextRange {
    return [IndexedRange rangeWithNSRange:_textView.markedTextRange];
}

The setter method for the selectedTextRange in Listing 10-8 simply sets the selected-text range on the embedded text view. The setMarkedText:selectedRange: method is more complex because, as you may recall, the range of marked text contains within it the range of selected text (even if the range merely identifies the caret), and these ranges have to be reconciled to reflect the situation after the insertion of text.

Listing 10-8  Setting the range of selected text and setting the marked text

- (void)setSelectedTextRange:(UITextRange *)range
{
    IndexedRange *r = (IndexedRange *)range;
    _textView.selectedTextRange = r.range;
}
 
- (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange {
    NSRange selectedNSRange = _textView.selectedTextRange;
    NSRange markedTextRange = _textView.markedTextRange;
 
    if (markedTextRange.location != NSNotFound) {
        if (!markedText)
            markedText = @"";
        [_text replaceCharactersInRange:markedTextRange withString:markedText];
        markedTextRange.length = markedText.length;
    } else if (selectedNSRange.length > 0) {
        [_text replaceCharactersInRange:selectedNSRange withString:markedText];
        markedTextRange.location = selectedNSRange.location;
        markedTextRange.length = markedText.length;
    } else {
        [_text insertString:markedText atIndex:selectedNSRange.location];
        markedTextRange.location = selectedNSRange.location;
        markedTextRange.length = markedText.length;
    }
    selectedNSRange = NSMakeRange(selectedRange.location + markedTextRange.location,
        selectedRange.length);
 
    _textView.text = _text;
    _textView.markedTextRange = markedTextRange;
    _textView.selectedTextRange = selectedNSRange;
}

Note that EditableCoreTextView replaces the text by calling the replaceCharactersInRange:withString: method on its mutable string object, which it then assigns to the text property of the embedded text view.

Frequently Called UITextInput Methods

When users type characters on the keyboard and when those characters enter text storage and are laid out, the text input system requests information from the object adopting the UITextInput protocol. Three of the more frequently called methods are textRangeFromPosition:toPosition:offsetFromPosition:toPosition:, andpositionFromPosition:offset:.

The text input system calls positionFromPosition:offset: to get the position in the text that’s a given offset from another position. Listing 10-9 shows howEditableCoreTextView implements this method (which includes range checking).

Listing 10-9  Implementing positionFromPosition:offset:

- (UITextPosition *)positionFromPosition:(UITextPosition *)position offset:(NSInteger)offset {
    IndexedPosition *pos = (IndexedPosition *)position;
    NSInteger end = pos.index + offset;
    if (end > _text.length || end < 0)
        return nil;
    return [IndexedPosition positionWithIndex:end];
}

The offsetFromPosition:toPosition: method should satisfy the opposite request and return a value specifying the offset between two text positions.EditableCoreTextView implements it as shown in Listing 10-10.

Listing 10-10  Implementing offsetFromPosition:toPosition:

- (NSInteger)offsetFromPosition:(UITextPosition *)from toPosition:(UITextPosition *)toPosition {
    IndexedPosition *f = (IndexedPosition *)from;
    IndexedPosition *t = (IndexedPosition *)toPosition;
    return (t.index - f.index);
}

Finally, the text input system frequently asks a text view for a text range that falls between two text positions. Listing 10-11 shows an implementation oftextRangeFromPosition:toPosition: that returns this range.

Listing 10-11  Implementing textRangeFromPosition:toPosition:

- (UITextRange *)textRangeFromPosition:(UITextPosition *)fromPosition
          toPosition:(UITextPosition *)toPosition {
    IndexedPosition *from = (IndexedPosition *)fromPosition;
    IndexedPosition *to = (IndexedPosition *)toPosition;
    NSRange range = NSMakeRange(MIN(from.index, to.index), ABS(to.index - from.index));
    return [IndexedRange rangeWithNSRange:range];
}
Returning Rectangles

When a correction bubble appears and when the user types in Japanese, the text input system sends firstRectForRange: and caretRectForPosition: to the text view. The purpose of both of these methods is to return a rectangle enclosing either a range of text or the caret that marks the insertion point. The EditableCoreTextView class implements the first of these methods by calling a method of its embedded text view that maps the range to an enclosing rectangle (see Listing 10-12). Before returning the rectangle, it converts it to the local coordinate system.

Listing 10-12  An implementation of firstRectForRange:

- (CGRect)firstRectForRange:(UITextRange *)range {
    IndexedRange *r = (IndexedRange *)range;
    CGRect rect = [_textView firstRectForNSRange:r.range];
    return [self convertRect:rect fromView:_textView];
}

The embedded text view in this case performs the lion’s share of the work. Using Core Text functions, it computes the rectangle that encloses the range of text and returns it, as shown in Listing 10-13.

Listing 10-13  Mapping text range to enclosing rectangle

- (CGRect)firstRectForNSRange:(NSRange)range; {
    int index = range.location;
    NSArray *lines = (NSArray *) CTFrameGetLines(_frame);
    for (int i = 0; i < [lines count]; i++) {
        CTLineRef line = (CTLineRef) [lines objectAtIndex:i];
        CFRange lineRange = CTLineGetStringRange(line);
        int localIndex = index - lineRange.location;
        if (localIndex >= 0 && localIndex < lineRange.length) {
            int finalIndex = MIN(lineRange.location + lineRange.length,
                range.location + range.length);
            CGFloat xStart = CTLineGetOffsetForStringIndex(line, index, NULL);
            CGFloat xEnd = CTLineGetOffsetForStringIndex(line, finalIndex, NULL);
            CGPoint origin;
            CTFrameGetLineOrigins(_frame, CFRangeMake(i, 0), &origin);
            CGFloat ascent, descent;
            CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
 
            return CGRectMake(xStart, origin.y - descent, xEnd - xStart, ascent + descent);
        }
    }
    return CGRectNull;
}

For caretRectForPosition:, the approach you take would be somewhat different. Selection affinity (selectionAffinity) is a factor to consider; more importantly, keep in mind that the height and width of the caret rectangle can be different from the bounding rectangle returned from firstRectForRange:.

Hit Testing

Another area where the text input system asks the text view to map between the display of text and the storage of text is hit testing. Given a point in the text view (the text input system asks), what is the corresponding text position or text range? The UITextInput methods it calls for this information are closestPositionToPoint:,closestPositionToPoint:withinRange:, and characterRangeAtPoint:Listing 10-14 illustrates how EditableCoreTextView implements the first of these methods.

Listing 10-14  An implementation of closestPositionToPoint:

- (UITextPosition *)closestPositionToPoint:(CGPoint)point {
    NSInteger index = [_textView closestIndexToPoint:point];
    return [IndexedPosition positionWithIndex:(NSUInteger)index];
}

Here, as with the methods that return rectangles for text ranges or text positions, EditableCoreTextView calls a method of its embedded view that uses Core Text to calculate the character index that corresponds to the point. Listing 10-15 illustrates how the embedded view accomplishes this.

Listing 10-15  Mapping a point to a character index

- (NSInteger)closestIndexToPoint:(CGPoint)point {
    NSArray *lines = (NSArray *) CTFrameGetLines(_frame);
    CGPoint origins[lines.count];
    CTFrameGetLineOrigins(_frame, CFRangeMake(0, lines.count), origins);
 
    for (int i = 0; i < lines.count; i++) {
        if (point.y > origins[i].y) {
            CTLineRef line = (CTLineRef) [lines objectAtIndex:i];
            return CTLineGetStringIndexForPosition(line, point);
        }
    }
    return  _text.length;
}
Informing the Text Input Delegate of Changes

When there is a change in text or a change in selection that is not initiated by the text input system, you should inform the text input delegate by sending it an appropriate “will-change” method. After making the change, send the delegate the corresponding “did-change” method.

The text input delegate is a system-provided object that adopts the UITextInputDelegate protocol. If the class adopting the UITextInput protocol defines aninputDelegate property, the text input system automatically assigns a delegate object to this property at runtime.

Listing 10-16 shows an action method that is invoked when the user taps in the text view. If the view is tapped but it isn’t the first responder, the text view makes itself the first responder and starts an editing session. If the view is subsequently tapped, the text view sends a selectionWillChange: message to the text input delegate. It then clears any marked text range and resets the selected text range so that the caret is at the point in the text where the tapped occurred. After this, it callsselectionDidChange:.

Listing 10-16  Sending messages to the text input delegate

- (void)tap:(UITapGestureRecognizer *)tap
{
    if (![self isFirstResponder]) {
        _textView.editing = YES;
        [self becomeFirstResponder];
    } else {
        [self.inputDelegate selectionWillChange:self];
 
        NSInteger index = [_textView closestIndexToPoint:[tap locationInView:_textView]];
        _textView.markedTextRange = NSMakeRange(NSNotFound, 0);
        _textView.selectedTextRange = NSMakeRange(index, 0);
 
        [self.inputDelegate selectionDidChange:self];
    }
}

Spell Checking and Word Completion

With an instance of the UITextChecker class, you can check the spelling of a document or offer suggestions for completing partially entered words. When spell-checking a document, a UITextChecker object searches a document at a specified offset. When it detects a misspelled word, it can also return an array of possible correct spellings, ranked in the order which they should be presented to the user (that is, the most likely replacement word comes first). You typically use a single instance of UITextCheckerper document, although you can use a single instance to spell-check related pieces of text if you want to share ignored words and other state.

Note: The UITextChecker class is intended for spell-checking and not for autocorrection. Autocorrection is a feature your text document can acquire by adopting the protocols and implementing the subclasses described in Communicating with the Text Input System.

The method you use for checking a document for misspelled words is rangeOfMisspelledWordInString:range:startingAt:wrap:language:; the method used for obtaining the list of possible replacement words is guessesForWordRange:inString:language:. You call these methods in the given order. To check an entire document, you call the two methods in a loop, resetting the starting offset to the character following the corrected word at each cycle through the loop, as shown in Listing 10-17.

Listing 10-17  Spell-checking a document

- (IBAction)spellCheckDocument:(id)sender {
    NSInteger currentOffset = 0;
    NSRange currentRange = NSMakeRange(0, 0);
    NSString *theText = textView.text;
    NSRange stringRange = NSMakeRange(0, theText.length-1);
    NSArray *guesses;
    BOOL done = NO;
 
    NSString *theLanguage = [[UITextChecker availableLanguages] objectAtIndex:0];
    if (!theLanguage)
        theLanguage = @"en_US";
 
    while (!done) {
        currentRange = [textChecker rangeOfMisspelledWordInString:theText range:stringRange
            startingAt:currentOffset wrap:NO language:theLanguage];
        if (currentRange.location == NSNotFound) {
            done = YES;
            continue;
        }
        guesses = [textChecker guessesForWordRange:currentRange inString:theText
            language:theLanguage];
        NSLog(@"---------------------------------------------");
        NSLog(@"Word misspelled is %@", [theText substringWithRange:currentRange]);
        NSLog(@"Possible replacements are %@", guesses);
        NSLog(@" ");
        currentOffset = currentOffset + (currentRange.length-1);
    }
}

The UITextChecker class includes methods for telling the text checker to ignore or learn words. Instead of just logging the misspelled words and their possible replacements, as the method in Listing 10-17 does, you should display some user interface that allows users to select correct spellings, tell the text checker to ignore or learn a word, and proceed to the next word without making any changes. One possible approach for an iPad app would be to use a popover view that lists the guesses in a table view and includes buttons such as Replace, Learn, Ignore, and so on.

You may also use UITextChecker to obtain completions for partially entered words and display the completions in a table view in a popover view. For this task, you call thecompletionsForPartialWordRange:inString:language: method, passing in the range in the given string to check. This method returns an array of possible words that complete the partially entered word. Listing 10-18 shows how you might call this method and display a table view listing the completions in a popover view.

Listing 10-18  Presenting a list of word completions for the current partial string

- (IBAction)completeCurrentWord:(id)sender {
 
    self.completionRange = [self computeCompletionRange];
    // The UITextChecker object is cached in an instance variable
    NSArray *possibleCompletions = [textChecker completionsForPartialWordRange:self.completionRange
        inString:self.textStore language:@"en"];
 
    CGSize popOverSize = CGSizeMake(150.0, 400.0);
    completionList = [[CompletionListController alloc] initWithStyle:UITableViewStylePlain];
    completionList.resultsList = possibleCompletions;
    completionListPopover = [[UIPopoverController alloc] initWithContentViewController:completionList];
    completionListPopover.popoverContentSize = popOverSize;
    completionListPopover.delegate = self;
    // rectForPartialWordRange: is a custom method
    CGRect pRect = [self rectForPartialWordRange:self.completionRange];
    [completionListPopover presentPopoverFromRect:pRect inView:self
        permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}

Feedback
修改这段代码以支持打包 import os import cv2 import numpy as np import screeninfo from PIL import Image, ImageDraw, ImageFont import time from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler import os import logging # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') def resource_path(relative_path): """获取资源绝对路径,支持PyInstaller打包后运行""" try: base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) class EnhancedImageViewer: """增强版图像查看器,支持亮度查看(Lux值)""" def __init__(self, image_path, title="图像查看器", base_output_name=None): self.image_path = image_path self.original_image = cv2.imread(image_path) if self.original_image is None: raise FileNotFoundError(f"无法加载图像: {image_path}") # 保存基础输出名称 self.base_output_name = base_output_name if base_output_name else \ os.path.splitext(os.path.basename(image_path))[0] # 获取灰度图像用于亮度分析 self.gray_image = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2GRAY) # 获取屏幕尺寸 try: screen = screeninfo.get_monitors()[0] self.max_width = screen.width - 100 self.max_height = screen.height - 100 except: self.max_width = 1620 self.max_height = 880 # 初始缩放状态 self.scale_factor = 1.0 self.offset_x = 0 self.offset_y = 0 # 创建显示图像 self.display_image = self.original_image.copy() self.resized_display = self.resize_to_fit(self.original_image) # 创建窗口 self.window_name = title cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL) cv2.setMouseCallback(self.window_name, self.mouse_callback) # 更新显示 self.update_display() def resize_to_fit(self, image): """调整图像尺寸以适应屏幕""" height, width = image.shape[:2] scale_width = self.max_width / width scale_height = self.max_height / height self.scale_factor = min(scale_width, scale_height, 1.0) new_width = int(width * self.scale_factor) new_height = int(height * self.scale_factor) if self.scale_factor < 1.0: return cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA) return image.copy() def put_chinese_text(self, image, text, position, font_size=20, color=(255, 255, 255)): """在图像上添加中文文本""" if image is None or image.size == 0: return image # 确保图像是3通道的 if len(image.shape) == 2: image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # 转换为PIL图像 (RGB格式) pil_img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(pil_img) # 使用支持中文的字体 try: font = ImageFont.truetype("simhei.ttf", font_size) except: try: font = ImageFont.truetype("msyh.ttc", font_size) except: try: font = ImageFont.truetype("/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf", font_size) except: font = ImageFont.load_default() # 添加文本 draw.text(position, text, font=font, fill=color) # 转换回OpenCV格式 return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) def update_display(self): """更新显示图像""" # 缩放小于等于1.0时直接显示 self.display_image = self.resized_display.copy() # 添加帮助文本 help_text = "左键:查看亮度(Lux) | ESC:退出 | R:重置 | S:保存" self.display_image = self.put_chinese_text( self.display_image, help_text, (10, 20), font_size=20, color=(0, 255, 0) ) # 显示图像 cv2.imshow(self.window_name, self.display_image) def mouse_callback(self, event, x, y, flags, param): """鼠标事件回调函数 - 仅保留左键点击功能""" # 鼠标左键点击 - 显示亮度值(Lux) if event == cv2.EVENT_LBUTTONDOWN: # 计算原始图像坐标 orig_x, orig_y = self.convert_coords(x, y) # 获取原始图像中的灰度值 if 0 <= orig_x < self.gray_image.shape[1] and 0 <= orig_y < self.gray_image.shape[0]: gray_value = self.gray_image[orig_y, orig_x] # 将灰度值转换为Lux值 (0-255范围映射到0-740 Lux) lux_value = int((gray_value / 255.0) * 740) # 在当前显示图像上添加标记和信息 display_copy = self.display_image.copy() cv2.circle(display_copy, (x, y), 5, (0, 0, 255), -1) # 计算文本位置(避免遮挡) text_x = x + 10 text_y = y - 10 # 如果靠近右侧边缘,向左移动文本 if text_x > self.display_image.shape[1] - 250: text_x = x - 250 # 如果靠近底部边缘,向上移动文本 if text_y < 30: text_y = y + 20 # 添加中文文本(显示Lux值) text = f"位置: ({orig_x}, {orig_y}) 亮度: {lux_value} Lux" display_copy = self.put_chinese_text( display_copy, text, (text_x, text_y), font_size=18, color=(0, 255, 255) ) cv2.imshow(self.window_name, display_copy) def convert_coords(self, x, y): """将显示坐标转换为原始图像坐标""" # 缩小或正常状态下的坐标转换 orig_x = int(x / self.scale_factor) orig_y = int(y / self.scale_factor) # 确保坐标在有效范围内 orig_x = max(0, min(orig_x, self.original_image.shape[1] - 1)) orig_y = max(0, min(orig_y, self.original_image.shape[0] - 1)) return orig_x, orig_y def run(self): """运行查看器主循环""" while True: key = cv2.waitKey(1) & 0xFF if key == 27: # ESC退出 break elif key == ord('r'): # 重置视图 self.scale_factor = 1.0 self.offset_x = 0 self.offset_y = 0 self.resized_display = self.resize_to_fit(self.original_image) self.update_display() elif key == ord('s'): # 保存当前视图 # 使用基础输出名称生成截图文件名 screenshot_path = f"{self.base_output_name}_screenshot.png" cv2.imwrite(screenshot_path, self.display_image) print(f"截图已保存为 {screenshot_path}") cv2.destroyAllWindows() def preprocess_image(image_path): """图像预处理""" image = cv2.imread(image_path) if image is None: raise FileNotFoundError(f"无法加载图像: {image_path}") # 转换为灰度图并降噪 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (9, 9), 0) # 对比度增强 clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(8, 8)) enhanced = clahe.apply(blurred) return image, gray, enhanced def analyze_intensity(enhanced): """亮度分析""" min_intensity = np.min(enhanced) max_intensity = np.max(enhanced) normalized = cv2.normalize(enhanced.astype('float'), None, 0, 1, cv2.NORM_MINMAX) return min_intensity, max_intensity, normalized def generate_heatmap(image, normalized): """生成热力图""" heatmap = cv2.applyColorMap((normalized * 255).astype(np.uint8), cv2.COLORMAP_JET) blended = cv2.addWeighted(image, 0.7, heatmap, 0.3, 0) return heatmap, blended def process_contours(image, enhanced, min_intensity, max_intensity, num_levels=7): """处理等高线""" height, width = image.shape[:2] contour_image = np.zeros_like(image) color_intensity_map = [] # 计算亮度层级 levels = np.linspace(min_intensity, max_intensity, num_levels).astype(np.uint8) kernel = np.ones((3, 3), np.uint8) for i, level in enumerate(levels): # 创建当前亮度层级的掩膜 lower_val = max(int(level) - 10, 0) upper_val = min(int(level) + 10, 255) mask = cv2.inRange(enhanced, lower_val, upper_val) # 形态学处理 mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 查找等高线 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 计算颜色(冷色调到暖色调渐变) normalized_level = (level - min_intensity) / (max_intensity - min_intensity) if normalized_level < 0.5: hue = 120 + 60 * normalized_level * 2 else: hue = 60 * (1 - (normalized_level - 0.5) * 2) hue = np.clip(hue, 0, 180) # 转换为BGR颜色 color = cv2.cvtColor(np.uint8([[[hue, 255, 255]]]), cv2.COLOR_HSV2BGR)[0][0] color = tuple(map(int, color)) # 存储颜色-亮度映射 # 将灰度值转换为Lux值 (0-255范围映射到0-740 Lux) lux_value = int((level / 255.0) * 740) color_intensity_map.append((color, lux_value)) # 绘制等高线 cv2.drawContours(contour_image, contours, -1, color, 2) return contour_image, color_intensity_map def create_color_bar(image, color_intensity_map, min_intensity, max_intensity): """创建颜色条 - 显示Lux值""" height, width = image.shape[:2] bar_width = int(width * 0.03) bar_height = int(height * 0.3) bar_x = width - int(width * 0.05) - bar_width bar_y = int(height * 0.05) # 生成颜色条 color_bar = np.zeros((bar_height, bar_width, 3), dtype=np.uint8) for i in range(bar_height): idx = int((1 - i / bar_height) * (len(color_intensity_map) - 1)) color_bar[i, :] = color_intensity_map[idx][0] # 添加到图像 result = image.copy() result[bar_y:bar_y + bar_height, bar_x:bar_x + bar_width] = color_bar # 添加边框 cv2.rectangle(result, (bar_x, bar_y), (bar_x + bar_width, bar_y + bar_height), (255, 255, 255), 1) # 添加刻度和标签(显示Lux值) num_ticks = 5 min_lux = 0 max_lux = 740 for i, pos in enumerate(np.linspace(0, bar_height, num_ticks)): y_pos = int(bar_y + pos) cv2.line(result, (bar_x - 5, y_pos), (bar_x, y_pos), (255, 255, 255), 1) # 计算标签位置 value = int(min_lux + (max_lux - min_lux) * (1 - pos / bar_height)) text_x = bar_x - 50 text_y = y_pos + (15 if i == 0 else -10 if i == num_ticks - 1 else 0) # 添加带描边的文本 cv2.putText(result, f"{value} Lux", (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 0), 2) cv2.putText(result, f"{value} Lux", (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1) # 添加标题 cv2.putText(result, 'Light Intensity (Lux)', (bar_x - 150, bar_y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 0), 2) cv2.putText(result, 'Light Intensity (Lux)', (bar_x - 150, bar_y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1) return result class ImageHandler(FileSystemEventHandler): def __init__(self, processing_function): self.processing_function = processing_function self.supported_exts = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff'] def on_created(self, event): """当新文件创建时触发""" if not event.is_directory: file_path = event.src_path if self._is_image(file_path): logging.info(f"检测到新图片: {file_path}") # 等待文件完全写入(根据实际需求调整) time.sleep(0.5) self.processing_function(file_path) def _is_image(self, file_path): """检查是否为支持的图片格式""" ext = os.path.splitext(file_path)[1].lower() return ext in self.supported_exts def start_monitoring(path_to_watch, processing_function): """启动文件夹监控服务""" event_handler = ImageHandler(processing_function) observer = Observer() observer.schedule(event_handler, path_to_watch, recursive=False) observer.start() logging.info(f"开始监控文件夹: {path_to_watch}") try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join() def main(image_path): """主处理流程""" try: # 从路径中提取基础文件名(不含扩展名) base_name = os.path.splitext(os.path.basename(image_path))[0] # 1. 图像预处理 image, gray, enhanced = preprocess_image(image_path) # 2. 亮度分析 min_intensity, max_intensity, normalized = analyze_intensity(enhanced) # 3. 生成热力图 heatmap, blended = generate_heatmap(image, normalized) # 4. 处理等高线 contour_image, color_intensity_map = process_contours( image, enhanced, min_intensity, max_intensity ) # 5. 创建最终结果(显示Lux值) base_image = cv2.addWeighted(image, 0.7, contour_image, 0.3, 0) # 使用固定的0-740 Lux范围 min_lux = 0 max_lux = 740 final_result = create_color_bar(base_image, color_intensity_map, min_lux, max_lux) # 保存结果(使用基础文件名作为前缀) cv2.imwrite(f"{base_name}_result.png", final_result) cv2.imwrite(f"{base_name}_Contours.png", contour_image) cv2.imwrite(f"{base_name}_Heatmap.png", heatmap) cv2.imwrite(f"{base_name}_Blended.png", blended) print(f"处理完成! 结果已保存为 {base_name}_*.png") # 启动交互式查看器(传递基础文件名) viewer = EnhancedImageViewer('4.jpg', 'photo_view', base_output_name=base_name) viewer.run() except Exception as e: print(f"处理过程中发生错误: {str(e)}") if __name__ == "__main__": # 修改这里的图片路径 image_path = '4.jpg' main(image_path)
07-15
之前我配置了libinjection.so到防火墙上,现在修改我发给你的代码,将modsecurity配置到防火墙上 app.py: from flask import Flask, request, jsonify import ctypes import numpy as np from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.sequence import pad_sequences import pickle import json from urllib.parse import unquote import html import sys import base64 import re from utils.makelog import log_detection import os import logging from logging.handlers import RotatingFileHandler os.environ['TF_KERAS'] = '1' os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # 1=警告,2=错误,3=静默 os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0' # 关闭 oneDNN 提示 app = Flask(__name__) log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'utils') os.makedirs(log_dir, exist_ok=True) # 配置文件日志处理器(10MB轮换,保留10个备份) file_handler = RotatingFileHandler( os.path.join(log_dir, 'app.log'), maxBytes=10*1024*1024, backupCount=10 ) file_handler.setFormatter(logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' )) # 设置日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL) app.logger.setLevel(logging.INFO) file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) # --- 加载 libinjection --- try: libinjection = ctypes.CDLL('/usr/local/lib/libinjection.so', mode=ctypes.RTLD_GLOBAL) libinjection.libinjection_sqli.argtypes = [ ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t ] libinjection.libinjection_sqli.restype = ctypes.c_int app.logger.info("Libinjection 加载成功") print("Libinjection 加载成功(控制台输出)") except Exception as e: app.logger.error(f"Libinjection 加载失败: {str(e)}", exc_info=True) exit(1) # --- 解码辅助函数 --- def try_base64_decode(s): try: if len(s) % 4 != 0: return s decoded = base64.b64decode(s).decode('utf-8', errors='ignore') if all(32 <= ord(c) <= 126 or c in '\t\r\n' for c in decoded): return decoded return s except Exception: return s def deep_url_decode(s, max_depth=3): decoded = s for _ in range(max_depth): new_decoded = unquote(decoded) if new_decoded == decoded: break decoded = new_decoded return decoded # --- 提取 HTTP 请求中的潜在 SQL 内容 --- def extract_sql_candidates(data): candidates = [] def extract_strings(obj): EXCLUDED_KEYS = {'uri', 'path', 'security', 'PHPSESSID', 'session_id','Login', 'login', 'submit', 'Submit'} STATIC_RESOURCES = {'.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2'} if isinstance(obj, dict): for key, value in obj.items(): if key in EXCLUDED_KEYS: continue # 检查值是否为静态资源(无需检测) if isinstance(value, str) and any(ext in value.lower() for ext in STATIC_RESOURCES): continue extract_strings(value) # 递归调用,仅传递值 elif isinstance(obj, list): for item in obj: extract_strings(item) elif isinstance(obj, str): text = obj # 多层 URL 解码 text = deep_url_decode(text) # HTML 实体解码 text = html.unescape(text) # Unicode 转义解码 try: text = text.encode().decode('unicode_escape') except Exception: pass # Base64 解码 text = try_base64_decode(text) if len(text) < 1000: candidates.append(text) extract_strings(data) return candidates # --- 检测逻辑 --- def detect_one(query): if re.match(r'^\/.*\.(php|html|js)$', query): return { "检测结果": "正常", "检测方式": "URI过滤", "可信度": 1.0 } result_buf = ctypes.create_string_buffer(8) is_libi_sqli = libinjection.libinjection_sqli(query.encode('utf-8'), len(query),result_buf,ctypes.sizeof(result_buf)) if is_libi_sqli: return { "检测结果": "存在SQL注入", "检测方式": "Libinjection", } else: return { "检测结果": "正常", "检测方式": "Libinjection", } @app.route('/') def home(): return "SQL 注入检测系统已启动" @app.route('/detect', methods=['POST']) def detect(): app.logger.info(f"接收到请求: {request.json}") try: data = request.get_json() if not data: return jsonify({"error": "缺少 JSON 请求体"}), 400 ip = request.remote_addr candidates = extract_sql_candidates(data) results = [] for query in candidates: result = detect_one(query) log_detection(ip, query, result) results.append(result) return jsonify({"detections": results}) except Exception as e: return jsonify({"error": f"检测过程中发生错误: {str(e)}"}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) nainx.conf: # 全局作用域(仅保留一份) user user; worker_processes 1; events { worker_connections 1024; } http { lua_package_path "/usr/local/openresty/lualib/?.lua;;"; include mime.types; default_type text/html; sendfile on; keepalive_timeout 65; server { listen 80; server_name 10.18.47.200; location /dvwa { rewrite_by_lua_file /usr/local/openresty/lualib/parse.lua; proxy_pass http://192.168.159.100/DVWA-master/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_redirect http://10.18.47.200/DVWA-master/ http://10.18.47.200/dvwa/; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; charset utf-8; } #屏蔽图标 location = /favicon.ico { access_log off; log_not_found off; } } } parse.lua: local cjson = require "cjson.safe" local http = require "resty.http" -- 1) 解析 Nginx 内置变量和 Headers local method = ngx.req.get_method() local uri = ngx.var.request_uri local headers = { user_agent = ngx.var.http_user_agent or "", cookie = ngx.var.http_cookie or "", host = ngx.var.http_host or "", content_type = ngx.var.http_content_type or "" } -- 2) 解析 GET 参数 ngx.req.read_body() -- 必须先读取 body,否则取不到 POST local args = ngx.req.get_uri_args() local query_params = {} for k, v in pairs(args) do query_params[k] = v end -- 3) 解析 POST 数据: 根据 content_type 判断JSON或表单 local post_data = {} if headers.content_type and string.find(headers.content_type, "application/json") then local body_data = ngx.req.get_body_data() if body_data then local json_data = cjson.decode(body_data) if json_data then post_data = json_data else ngx.log(ngx.ERR, "JSON 解析失败") end end else local post_args = ngx.req.get_post_args() for k, v in pairs(post_args) do post_data[k] = v end end -- 4) 整合请求数据并日志输出 local request_data = { method = method, uri = uri, headers = headers, query_params = query_params, post_data = post_data, client_ip = ngx.var.remote_addr } ngx.log(ngx.ERR, "OpenResty 解析的数据: " .. cjson.encode(request_data)) -- 5) 调用 Flask WAF 后端 local httpc = http.new() local res, err = httpc:request_uri("http://127.0.0.1:5000/detect", { method = "POST", body = cjson.encode(request_data), headers = { ["Content-Type"] = "application/json" } }) if not res then ngx.log(ngx.ERR, "Flask WAF 请求失败: ", err) ngx.status = 500 ngx.header["Content-Type"] = "text/html; charset=utf-8" ngx.say("WAF 检测异常") return ngx.exit(500) end -- 6) 复用连接 local ok, err_keep = httpc:set_keepalive(60000, 100) if not ok then ngx.log(ngx.ERR, "设置 keepalive 失败: ", err_keep) end ngx.log(ngx.ERR, "Flask 返回: ", res.body) -- 7) 解析Flask响应并处理(修正pcall返回值) if res.status ~= 200 then ngx.log(ngx.ERR, "Flask 返回非200状态码: ", res.status) ngx.status = 500 ngx.header["Content-Type"] = "text/html; charset=utf-8" ngx.say("Flask 服务异常") return ngx.exit(500) end local success, decoded_data = pcall(cjson.decode, res.body) if not success then ngx.log(ngx.ERR, "Flask 响应JSON解析失败: ", decoded_data) ngx.status = 500 ngx.header["Content-Type"] = "text/html; charset=utf-8" ngx.say("WAF 响应格式错误") return ngx.exit(500) end local waf_result = decoded_data -- 8) 判断是否存在SQL注入(根据app.py的响应结构) local is_sqli = false local detections = waf_result.detections or {} for i = 1, #detections do local detection = detections[i] -- 检查检测结果是否为表类型且包含检测结果字段 if type(detection) == "table" and detection["检测结果"] then if detection["检测结果"] == "存在SQL注入" then is_sqli = true break end end end -- for _, detection in ipairs(waf_result.detections or {}) do -- if detection["检测结果"] == "存在SQL注入" then -- is_sqli = true -- break -- end -- end -- 9) 根据检测结果决定是否拦截 if is_sqli then ngx.log(ngx.ERR, "WAF阻断 SQL注入") ngx.status = ngx.HTTP_FORBIDDEN ngx.header["Content-Type"] = "text/html; charset=utf-8" ngx.say([[ <!DOCTYPE html> <html lang="zh"> <head> <meta charset="utf-8"/> <title>访问受限</title> <style> /* 全局样式重置 */ * { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #000; color: #fff; font-family: "Microsoft YaHei", Arial, sans-serif; } .container { width: 90%; max-width: 600px; padding: 40px; text-align: center; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; background: rgba(10, 10, 10, 0.8); backdrop-filter: blur(5px); box-shadow: 0 0 15px rgba(255, 255, 255, 0.05); } h1 { margin-bottom: 20px; font-size: 2.5rem; letter-spacing: 2px; color: #fff; text-shadow: 0 0 10px rgba(0, 255, 255, 0.5); } p { margin-bottom: 30px; font-size: 1.1rem; line-height: 1.8; color: rgba(255, 255, 255, 0.9); } .btn-back { display: inline-block; padding: 12px 30px; background: transparent; color: #00ffcc; border: 1px solid #00ffcc; border-radius: 4px; font-size: 1rem; font-weight: bold; text-decoration: none; transition: all 0.3s ease; cursor: pointer; } .btn-back:hover { background: rgba(0, 255, 204, 0.1); box-shadow: 0 0 15px rgba(0, 255, 204, 0.3); transform: translateY(-2px); } </style> </head> <body> <div class="container"> <h1>访问受限</h1> <p> 检测到疑似SQL注入/跨站脚本攻击(XXS)的恶意行为特征,<br/> 为保障系统安全,本次请求未被授权执行<br/> </p> <a href="javascript:void(0)" class="btn-back" id="backBtn">返回上一页</a> </div> <script> // 点击按钮返回前一个页面状态 document.getElementById('backBtn').addEventListener('click', function() { history.back(); }); </script> </body> ]]) else ngx.log(ngx.ERR, "WAF 判断正常,放行请求") return -- 关键:放行请求,继续执行proxy_pass end
06-29
class SCLMultiProcessor: def __init__(self, root): self.root = root # 创建日志目录 self.log_dir = "logs" self.root.title("SCL文件处理系统") self.root.geometry("1100x750") # 增加窗口尺寸以适应新控件 # 初始化变量 self.color_detector = EnhancedColorDetector() self.stats_processor = SCLRuleProcessor(self.color_detector) self.empty_cell_detector = EnhancedEmptyCellDetector(self.color_detector) self.progress_var = tk.DoubleVar() os.makedirs(self.log_dir, exist_ok=True) # 初始化日志系统 self.current_log_file = None self.setup_logger() # 创建主框架 self.main_frame = ttk.Frame(root, padding="10") self.main_frame.pack(fill=tk.BOTH, expand=True) # 创建UI self.create_ui() # 记录UI初始化完成 logger.info("用户界面初始化完成") # 先定义 toggle_config_fields 方法 def toggle_config_fields(self): """根据操作模式显示/隐藏相关配置字段""" mode = self.operation_mode.get() # 统计模式:显示统计表路径,隐藏CheckSheet路径 if mode == "stats": self.input_frame.pack(fill=tk.X, pady=5) # 显示统计表路径 self.checksheet_frame.pack_forget() # 隐藏CheckSheet路径 logger.info("切换到统计模式,显示统计表路径") # SCL格式检查模式:隐藏统计表路径,显示CheckSheet路径 elif mode == "empty_check": self.input_frame.pack_forget() # 隐藏统计表路径 self.checksheet_frame.pack(fill=tk.X, pady=5) # 显示CheckSheet路径 logger.info("切换到SCL格式检查模式,显示CheckSheet路径") def create_ui(self): """创建用户界面""" # 操作模式选择区域 - 放在最前面 mode_frame = ttk.LabelFrame(self.main_frame, text="操作模式", padding="10") mode_frame.pack(fill=tk.X, pady=5) # 添加操作模式单选按钮 self.operation_mode = tk.StringVar(value="stats") # 默认选择统计模式 ttk.Radiobutton(mode_frame, text="统计功能", variable=self.operation_mode, value="stats", command=self.toggle_config_fields).pack(side=tk.LEFT, padx=10) ttk.Radiobutton(mode_frame, text="SCL格式检查", variable=self.operation_mode, value="empty_check", command=self.toggle_config_fields).pack(side=tk.LEFT, padx=10) # 文件选择区域 - 放在操作模式后面 file_frame = ttk.LabelFrame(self.main_frame, text="文件选择", padding="10") file_frame.pack(fill=tk.X, pady=5) # 输入文件选择 - 统计表路径(统计模式需要) self.input_frame = ttk.Frame(file_frame) ttk.Label(self.input_frame, text="统计表:").pack(side=tk.LEFT, padx=5) self.input_path_var = tk.StringVar() input_entry = ttk.Entry(self.input_frame, textvariable=self.input_path_var, width=70) input_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) ttk.Button(self.input_frame, text="浏览...", command=self.browse_input_file).pack(side=tk.LEFT, padx=5) # 配置区域 config_frame = ttk.LabelFrame(self.main_frame, text="处理配置", padding="10") config_frame.pack(fill=tk.X, pady=5) # 添加SCL文件夹路径输入 scl_folder_frame = ttk.Frame(config_frame) scl_folder_frame.pack(fill=tk.X, pady=5) ttk.Label(scl_folder_frame, text="SCL文件夹路径:").grid(row=0, column=0, padx=5, sticky=tk.W) self.scl_folder_var = tk.StringVar() scl_folder_entry = ttk.Entry(scl_folder_frame, textvariable=self.scl_folder_var, width=60) scl_folder_entry.grid(row=0, column=1, padx=5, sticky=tk.W) ttk.Button(scl_folder_frame, text="浏览...", command=self.browse_scl_folder).grid(row=0, column=2, padx=5, sticky=tk.W) # 搜索选项 search_frame = ttk.Frame(config_frame) search_frame.pack(fill=tk.X, pady=5) ttk.Label(search_frame, text="文件前缀:").grid(row=0, column=0, padx=5, sticky=tk.W) self.prefix_var = tk.StringVar(value="SCL_") ttk.Entry(search_frame, textvariable=self.prefix_var, width=10).grid(row=0, column=1, padx=5, sticky=tk.W) # 添加CheckSheet路径输入(SCL格式检查模式需要) self.checksheet_frame = ttk.Frame(config_frame) ttk.Label(self.checksheet_frame, text="CheckSheet路径:").grid(row=0, column=0, padx=5, sticky=tk.W) self.checksheet_path_var = tk.StringVar() checksheet_entry = ttk.Entry(self.checksheet_frame, textvariable=self.checksheet_path_var, width=60) checksheet_entry.grid(row=0, column=1, padx=5, sticky=tk.W) ttk.Button(self.checksheet_frame, text="浏览...", command=self.browse_checksheet_path).grid(row=0, column=2, padx=5, sticky=tk.W) # 添加性能提示 ttk.Label(config_frame, text="(表头固定在第3行,数据从第4行开始)").pack(anchor=tk.W, padx=5, pady=2) # 日志选项 log_frame = ttk.Frame(config_frame) log_frame.pack(fill=tk.X, pady=5) ttk.Label(log_frame, text="日志级别:").grid(row=0, column=0, padx=5, sticky=tk.W) self.log_level_var = tk.StringVar(value="INFO") log_level_combo = ttk.Combobox( log_frame, textvariable=self.log_level_var, width=10, state="readonly" ) log_level_combo['values'] = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') log_level_combo.grid(row=0, column=1, padx=5, sticky=tk.W) log_level_combo.bind("<<ComboboxSelected>>", self.change_log_level) # 根据初始模式显示/隐藏相关字段 self.toggle_config_fields() # 处理按钮 btn_frame = ttk.Frame(self.main_frame) btn_frame.pack(fill=tk.X, pady=10) ttk.Button(btn_frame, text="开始处理", command=self.process_file).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="查看日志", command=self.view_log).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="导出配置", command=self.export_config).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="加载配置", command=self.load_config).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="退出", command=self.root.destroy).pack(side=tk.RIGHT, padx=5) # 进度条 progress_frame = ttk.Frame(self.main_frame) progress_frame.pack(fill=tk.X, pady=5) ttk.Label(progress_frame, text="处理进度:").pack(side=tk.LEFT, padx=5) self.progress_bar = ttk.Progressbar( progress_frame, variable=self.progress_var, maximum=100, length=700 ) self.progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) self.progress_label = ttk.Label(progress_frame, text="0%") self.progress_label.pack(side=tk.LEFT, padx=5) # 结果展示区域 result_frame = ttk.LabelFrame(self.main_frame, text="处理结果", padding="10") result_frame.pack(fill=tk.BOTH, expand=True, pady=5) # 结果文本框 self.result_text = scrolledtext.ScrolledText( result_frame, wrap=tk.WORD, height=20 ) self.result_text.pack(fill=tk.BOTH, expand=True) self.result_text.config(state=tk.DISABLED) # 状态栏 self.status_var = tk.StringVar(value="就绪") status_bar = ttk.Label(self.main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(fill=tk.X, pady=5) logger.info("UI创建完成") def browse_checksheet_path(self): """浏览CheckSheet文件夹""" folder_path = filedialog.askdirectory(title="选择CheckSheet文件夹") if folder_path: self.checksheet_path_var.set(folder_path) logger.info(f"已选择CheckSheet文件夹: {folder_path}") def setup_logger(self): """配置日志记录器""" # 创建唯一日志文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") self.current_log_file = os.path.join(self.log_dir, f"scl_processor_{timestamp}.log") # 创建或获取日志记录器 self.logger = logging.getLogger("SCLProcessor") self.logger.setLevel(logging.INFO) # 移除所有现有处理器 for handler in self.logger.handlers[:]: self.logger.removeHandler(handler) # 创建文件处理器 file_handler = logging.FileHandler(self.current_log_file, encoding='utf-8') file_handler.setLevel(logging.INFO) # 创建控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # 创建日志格式 formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) # 添加处理器 self.logger.addHandler(file_handler) self.logger.addHandler(console_handler) # 记录日志初始化信息 self.logger.info(f"日志系统已初始化,日志文件: {self.current_log_file}") self.logger.info(f"日志目录: {os.path.abspath(self.log_dir)}") def change_log_level(self, event=None): """动态更改日志级别并更新所有处理器(会话感知版本)""" try: # 获取选择的日志级别 level_str = self.log_level_var.get() log_level = getattr(logging, level_str.upper()) # 更新全局日志级别设置 self.current_log_level = log_level logger.info(f"请求更改日志级别为: {level_str}") # 设置根日志记录器级别 logger.setLevel(log_level) # 更新所有现有处理器的级别 for handler in logger.handlers: # 仅更新文件和控制台处理器 if isinstance(handler, (logging.FileHandler, logging.StreamHandler)): handler.setLevel(log_level) # 记录级别变更确认 logger.info(f"日志级别已成功更改为: {level_str}") # 添加调试信息显示当前处理器级别 handler_levels = [ f"{type(h).__name__}: {logging.getLevelName(h.level)}" for h in logger.handlers ] logger.debug(f"当前处理器级别: {', '.join(handler_levels)}") # 更新UI状态显示 self.status_var.set(f"日志级别: {level_str}") except AttributeError: logger.error(f"无效的日志级别: {level_str}") messagebox.showerror("错误", f"无效的日志级别: {level_str}") except Exception as e: logger.exception("更改日志级别时发生错误") messagebox.showerror("错误", f"更改日志级别失败: {str(e)}") def browse_input_file(self): """浏览输入文件""" file_path = filedialog.askopenfilename( filetypes=[("Excel 文件", "*.xlsx *.xls"), ("所有文件", "*.*")] ) if file_path: self.input_path_var.set(file_path) self.input_file = file_path logger.info(f"已选择输入文件: {file_path}") def browse_scl_folder(self): """浏览SCL文件夹""" folder_path = filedialog.askdirectory(title="选择SCL文件夹") if folder_path: self.scl_folder_var.set(folder_path) logger.info(f"已选择SCL文件夹: {folder_path}") def highlight_cell(self, sheet, row, col, color="FFFF0000"): """为单元格设置背景色""" try: fill = PatternFill(start_color=color, end_color=color, fill_type="solid") sheet.cell(row=row, column=col).fill = fill return True except Exception as e: logger.error(f"设置单元格颜色失败: {str(e)}") return False def process_file(self): """处理文件 - 每次处理保存数据,下次运行重新开始""" operation_mode = self.operation_mode.get() # 统计模式需要统计表路径 if operation_mode == "stats" and not self.input_path_var.get(): messagebox.showwarning("警告", "请先选择统计表") logger.warning("未选择统计表") return # SCL格式检查模式需要CheckSheet路径 if operation_mode == "empty_check" and not self.checksheet_path_var.get(): messagebox.showwarning("警告", "请先选择CheckSheet路径") logger.warning("未选择CheckSheet路径") return try: # 重置结果 self.result_text.config(state=tk.NORMAL) self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, "开始处理...\n") self.result_text.see(tk.END) self.result_text.config(state=tk.DISABLED) self.status_var.set("开始处理文件...") self.root.update() # 每次处理前重新初始化日志系统 self.setup_logger() # 记录处理开始信息 self.logger.info("=" * 50) self.logger.info(f"开始新处理会话: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") self.logger.info("=" * 50) # 更新UI显示当前日志文件 self.status_var.set(f"当前日志: {os.path.basename(self.current_log_file)}") # 获取输入文件目录 input_file = self.input_path_var.get() input_dir = os.path.dirname(input_file) logger.info(f"开始处理文件: {input_file}") logger.info(f"文件目录: {input_dir}") # 获取SCL文件夹路径 scl_folder = self.scl_folder_var.get() if not scl_folder: # 如果没有指定SCL文件夹,则使用输入文件所在目录 scl_folder = input_dir logger.info(f"未指定SCL文件夹,使用输入文件目录: {scl_folder}") # 使用openpyxl加载工作簿(保留格式) wb = openpyxl.load_workbook(input_file) sheet = wb.active logger.info(f"工作簿加载成功, 工作表: {sheet.title}") # 获取配置参数 prefix = self.prefix_var.get() operation_mode = self.operation_mode.get() logger.info(f"配置参数: 文件前缀={prefix}, 操作模式={operation_mode}") # 扫描E列(第5列) total_rows = sheet.max_row processed_count = 0 found_files = 0 problem_files = 0 logger.info(f"开始扫描E列, 总行数: {total_rows}") start_time = time.time() for row_idx in range(1, total_rows + 1): # 更新进度 progress = (row_idx / total_rows) * 100 self.progress_var.set(progress) self.progress_label.config(text=f"{progress:.1f}%") self.root.update() cell = sheet.cell(row=row_idx, column=5) cell_value = str(cell.value) if cell.value else "" # 检查是否包含前缀的文件名 if prefix in cell_value: # 提取文件名(可能有多个以空格分隔) file_names = re.findall(fr'{prefix}[^\s]+', cell_value) logger.info(f"行 {row_idx}: 找到文件: {', '.join(file_names)}") result_lines = [] file_has_problems = False # 标记当前行是否有问题文件 for file_name in file_names: # 构建文件路径 - 使用SCL文件夹 file_path = os.path.join(scl_folder, file_name) # 检查文件是否存在 if not os.path.exists(file_path): result_lines.append(f"{file_name}: 文件不存在") logger.warning(f"文件不存在: {file_path}") # 标记文件不存在的单元格为紫色 self.highlight_cell(sheet, row_idx, 5, "FF800080") file_has_problems = True problem_files += 1 continue # 根据操作模式执行不同处理 if operation_mode == "stats": # 统计模式 results, color_report, missing_data = self.stats_processor.process_file(file_path) # 如果有数据缺失 if missing_data: file_has_problems = True problem_files += 1 result_lines.append(f"{file_name}: 数据缺失!") for item in missing_data: result_lines.append(f" - {item['message']}") logger.warning(item['message']) else: result_lines.append(f"{file_name}: 处理完成") # 将结果写入主Excel文件的不同列 for rule_name, result_str in results.items(): target_col = self.stats_processor.RULE_MAPPING.get(rule_name) if target_col: target_cell = sheet.cell(row=row_idx, column=target_col) target_cell.value = result_str elif operation_mode == "empty_check": # SCL格式检查模式 checksheet_path = self.checksheet_path_var.get() missing_data, marked_file_path = self.empty_cell_detector.detect_empty_cells( file_path, checksheet_path ) if missing_data: file_has_problems = True problem_files += 1 result_lines.append(f"{file_name}: 发现空单元格!") for item in missing_data: result_lines.append(f" - {item['message']}") logger.warning(item['message']) # 添加标记文件路径信息 if marked_file_path: result_lines.append(f"已生成标记文件: {marked_file_path}") else: result_lines.append(f"{file_name}: 无空单元格问题") found_files += 1 # 如果该行有文件存在问题,将E列单元格标红 if file_has_problems: self.highlight_cell(sheet, row_idx, 5) logger.info(f"行 {row_idx} E列单元格标记为红色(存在问题)") # 更新结果文本框 self.result_text.config(state=tk.NORMAL) self.result_text.insert( tk.END, f"行 {row_idx} 处理结果:\n" + "\n".join(result_lines) + "\n\n" ) self.result_text.see(tk.END) self.result_text.config(state=tk.DISABLED) processed_count += 1 # 保存修改后的Excel文件 - 每次处理保存数据 output_path = input_file.replace(".xlsx", "_processed.xlsx") wb.save(output_path) logger.info(f"结果已保存到: {output_path}") elapsed_time = time.time() - start_time status_msg = f"处理完成! 处理了 {processed_count} 个文件项, 耗时 {elapsed_time:.2f} 秒" if problem_files > 0: status_msg += f", {problem_files} 个文件存在问题" self.status_var.set(status_msg) logger.info(status_msg) # 更新结果文本框 self.result_text.config(state=tk.NORMAL) self.result_text.insert( tk.END, f"\n{status_msg}\n" f"结果已保存到: {output_path}\n" ) self.result_text.see(tk.END) self.result_text.config(state=tk.DISABLED) messagebox.showinfo("完成", status_msg) except Exception as e: error_msg = f"处理文件时出错: {str(e)}" logger.exception(f"处理文件时出错: {str(e)}") messagebox.showerror("错误", error_msg) self.status_var.set(f"错误: {str(e)}") # 更新结果文本框 self.result_text.config(state=tk.NORMAL) self.result_text.insert(tk.END, f"\n错误: {error_msg}\n") self.result_text.see(tk.END) self.result_text.config(state=tk.DISABLED) def view_log(self): """查看日志""" log_window = tk.Toplevel(self.root) log_window.title("处理日志") log_window.geometry("800x600") log_frame = ttk.Frame(log_window, padding="10") log_frame.pack(fill=tk.BOTH, expand=True) # 日志文本框 log_text = scrolledtext.ScrolledText( log_frame, wrap=tk.WORD, height=30 ) log_text.pack(fill=tk.BOTH, expand=True) # 读取日志文件 log_file = 'scl_processor.log' try: if not os.path.exists(log_file): with open(log_file, 'w', encoding='utf-8') as f: f.write("日志文件已创建,暂无记录\n") with open(log_file, 'r', encoding='utf-8') as f: log_content = f.read() log_text.insert(tk.END, log_content) except Exception as e: log_text.insert(tk.END, f"无法读取日志文件: {str(e)}") # 设置为只读 log_text.config(state=tk.DISABLED) # 添加刷新按钮 refresh_btn = ttk.Button(log_frame, text="刷新日志", command=lambda: self.refresh_log(log_text)) refresh_btn.pack(pady=5) logger.info("日志查看窗口已打开") def refresh_log(self, log_text): """刷新日志内容""" log_text.config(state=tk.NORMAL) log_text.delete(1.0, tk.END) try: with open('scl_processor.log', 'r', encoding='utf-8') as f: log_content = f.read() log_text.insert(tk.END, log_content) except Exception as e: log_text.insert(tk.END, f"刷新日志失败: {str(e)}") log_text.config(state=tk.DISABLED) log_text.see(tk.END) logger.info("日志已刷新") def export_config(self): """导出配置到文件""" config = { "prefix": self.prefix_var.get(), "log_level": self.log_level_var.get(), "operation_mode": self.operation_mode.get(), "tolerance": self.tolerance_var.get(), "checksheet_path": self.checksheet_path_var.get(), "scl_folder": self.scl_folder_var.get() # 添加SCL文件夹路径 } file_path = filedialog.asksaveasfilename( defaultextension=".json", filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")] ) if file_path: try: with open(file_path, 'w', encoding='utf-8') as f: f.write(str(config)) messagebox.showinfo("成功", f"配置已导出到: {file_path}") logger.info(f"配置已导出到: {file_path}") except Exception as e: messagebox.showerror("错误", f"导出配置失败: {str(e)}") logger.error(f"导出配置失败: {str(e)}") def load_config(self): """从文件加载配置""" file_path = filedialog.askopenfilename( filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")] ) if file_path: try: with open(file_path, 'r', encoding='utf-8') as f: config = eval(f.read()) self.prefix_var.set(config.get("prefix", "SCL_")) self.log_level_var.set(config.get("log_level", "INFO")) self.operation_mode.set(config.get("operation_mode", "stats")) self.tolerance_var.set(config.get("tolerance", 30)) self.checksheet_path_var.set(config.get("checksheet_path", "")) self.scl_folder_var.set(config.get("scl_folder", "")) # 加载SCL文件夹路径 self.change_log_level() messagebox.showinfo("成功", "配置已加载") logger.info(f"配置已从 {file_path} 加载") except Exception as e: messagebox.showerror("错误", f"加载配置失败: {str(e)}") logger.error(f"加载配置失败: {str(e)}") 隔离出SCL格式检查模式,检查SCL文件夹路径下的所有的SCL_前缀的文件,不依赖统计表
08-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值